<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>David Mohl</title>
    <link>https://david.coffee/</link>
    <description>Recent content on David Mohl</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <lastBuildDate>Tue, 16 Jul 2999 10:00:00 +0900</lastBuildDate><atom:link href="https://david.coffee/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>I Priced My Dotfiles Syncing App Wrong (and Other Lessons)</title>
      <link>https://david.coffee/configmesh-1-1/</link>
      <pubDate>Thu, 26 Feb 2026 10:00:00 +0900</pubDate>
      
      <guid>https://david.coffee/configmesh-1-1/</guid><media:content url="https://david.coffee/configmesh-1-1/banner.png" medium="image" />
          <enclosure url="https://david.coffee/configmesh-1-1/banner.png" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/configmesh-1-1/banner.png&#34; /&gt;&lt;/p&gt;&lt;p&gt;A month ago, I &lt;a href=&#34;https://david.coffee/configmesh-release/&#34;&gt;released ConfigMesh&lt;/a&gt;, a native macOS app that syncs your dotfiles and app settings across machines with end-to-end encryption. I spent months building what I thought was a solid product: zero-knowledge architecture, smart diffing, version history, granular control over what gets synced.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./screenshot.png&#34; alt=&#34;ConfigMesh&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Then I launched. I ran a &lt;a href=&#34;https://devhunt.org&#34;&gt;DevHunt&lt;/a&gt; launch, Reddit sponsored posts, Twitter boosts. Traffic came. Downloads didn&amp;rsquo;t. People were landing on the site, reading the feature list, and bouncing. Not because they didn&amp;rsquo;t understand what ConfigMesh does, but because the offer itself wasn&amp;rsquo;t landing.&lt;/p&gt;
&lt;p&gt;After a month of feedback, conversations, and some honest self-reflection, I shipped ConfigMesh 1.1. This post is about the two assumptions I got wrong and what I changed.&lt;/p&gt;
&lt;h2 id=&#34;nobody-wants-to-pay-5month-for-text-files&#34;&gt;Nobody wants to pay $5/month for text files&lt;/h2&gt;
&lt;p&gt;I launched ConfigMesh at $5/month (or $50/year). From my side of the screen, that felt reasonable. I&amp;rsquo;d built encryption infrastructure, a sync server, a native macOS app, months of development. Five bucks a month for all of that? Seemed fair.&lt;/p&gt;
&lt;p&gt;From the user&amp;rsquo;s side of the screen, the math looked very different. Dotfiles are tiny. We&amp;rsquo;re talking kilobytes. A typical user&amp;rsquo;s entire config collection is smaller than a single iPhone photo. Paying $5/month to sync that felt disproportionate, regardless of the engineering underneath.&lt;/p&gt;
&lt;p&gt;This is the indie dev pricing trap: you price based on what it cost you to build, not on what people are willing to pay. The perceived value of syncing text files has a hard ceiling, and $5/month was above it. It didn&amp;rsquo;t matter that the encryption was best-in-class or that the sync engine was smart. The sticker price created friction before anyone got to experience any of that.&lt;/p&gt;
&lt;p&gt;The clearest signal came from Reddit. I posted ConfigMesh on the Mac apps subreddit and the feedback split into two camps. Some people said the price was just too high for what it does. Others went further: they didn&amp;rsquo;t want another subscription at all, regardless of price. That second group was the more interesting signal. It&amp;rsquo;s not about the dollar amount, it&amp;rsquo;s about the model.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ConfigMesh 1.1 drops the price to $1.99/month or $19.99/year.&lt;/strong&gt; That&amp;rsquo;s a significant cut, but it better matches where users anchor the value of this kind of tool. More importantly, it&amp;rsquo;s no longer the only option (more on that below).&lt;/p&gt;
&lt;h2 id=&#34;trust-isnt-a-technical-problem&#34;&gt;Trust isn&amp;rsquo;t a technical problem&lt;/h2&gt;
&lt;p&gt;This was the more interesting lesson.&lt;/p&gt;
&lt;p&gt;ConfigMesh uses end-to-end encryption with a key that only you hold. The server literally cannot read your data. From a cryptographic standpoint, it doesn&amp;rsquo;t matter where the data is stored because it&amp;rsquo;s indistinguishable from random noise without your key.&lt;/p&gt;
&lt;p&gt;I assumed that would be enough. It wasn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;The same Reddit thread made this clear. People weren&amp;rsquo;t just pushing back on the price, they were pushing back on the storage model. When I mentioned I was working on a Dropbox backend, the interest shifted noticeably. That was the option people actually wanted. Not cheaper cloud storage, but their own storage.&lt;/p&gt;
&lt;p&gt;This isn&amp;rsquo;t irrational. It&amp;rsquo;s how people think about sensitive data. Ownership and control are emotional, not just technical. &amp;ldquo;Your data is encrypted on our servers&amp;rdquo; will always feel different from &amp;ldquo;your data is on your Dropbox,&amp;rdquo; even when the former is arguably more secure. You can&amp;rsquo;t engineer your way past that.&lt;/p&gt;
&lt;p&gt;The deeper realization was that some users don&amp;rsquo;t need managed cloud storage at all. They already have storage (Dropbox, iCloud, NAS). They don&amp;rsquo;t want another cloud backend. They just want the sync engine.&lt;/p&gt;
&lt;h2 id=&#34;enter-byos-bring-your-own-storage&#34;&gt;Enter BYOS (Bring Your Own Storage)&lt;/h2&gt;
&lt;p&gt;ConfigMesh 1.1 adds a one-time purchase option with no included cloud storage. Instead, you sync against your own Dropbox.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./provider-selector.png&#34; alt=&#34;Selecting Dropbox as your storage provider&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;The tradeoffs are real: Dropbox is slower than the managed backend and more error-prone. Sync conflicts are harder to resolve gracefully when you don&amp;rsquo;t control the storage layer. It&amp;rsquo;s a compromise, but it&amp;rsquo;s the compromise users actually asked for.&lt;/p&gt;
&lt;p&gt;And yes, your data on Dropbox is still fully encrypted. Here&amp;rsquo;s what it actually looks like on disk: just encrypted blobs, completely unreadable without your key.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./dropbox-blobs.png&#34; alt=&#34;Encrypted blobs in Dropbox&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how the tiers work now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Subscription ($1.99/month or $19.99/year):&lt;/strong&gt; Fully managed, encrypted cloud storage. Fastest sync, most reliable. Same as before, just cheaper.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One-time purchase:&lt;/strong&gt; No cloud storage included. Sync against your own Dropbox. Slower, but you own everything.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;rsquo;re on the one-time purchase tier and try to use the managed ConfigMesh backend, you&amp;rsquo;ll see a clear upgrade prompt instead of a confusing error.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./upgrade-required.png&#34; alt=&#34;Upgrade required dialog&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Dropbox is the first supported provider. More are coming.&lt;/p&gt;
&lt;h2 id=&#34;what-id-tell-myself-at-launch&#34;&gt;What I&amp;rsquo;d tell myself at launch&lt;/h2&gt;
&lt;p&gt;Ship your pricing assumptions as cheaply as possible. I could have launched with a lower price and a free trial and validated willingness-to-pay before building conviction around $5/month.&lt;/p&gt;
&lt;p&gt;And when users tell you they don&amp;rsquo;t trust something, don&amp;rsquo;t argue with their reasoning. Just give them an alternative they&amp;rsquo;re comfortable with. The best feature you can build is sometimes just getting out of the way.&lt;/p&gt;
&lt;p&gt;ConfigMesh 1.1 is available now at &lt;a href=&#34;https://configmesh.app&#34;&gt;configmesh.app&lt;/a&gt;. If you tried it before and bounced on the price or the storage model, give it another look.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/configmesh-1-1/banner.png" /></p><p>A month ago, I <a href="https://david.coffee/configmesh-release/">released ConfigMesh</a>, a native macOS app that syncs your dotfiles and app settings across machines with end-to-end encryption. I spent months building what I thought was a solid product: zero-knowledge architecture, smart diffing, version history, granular control over what gets synced.</p>
<p>
<img class="img-fluid" src="./screenshot.png" alt="ConfigMesh" />

</p>
<p>Then I launched. I ran a <a href="https://devhunt.org">DevHunt</a> launch, Reddit sponsored posts, Twitter boosts. Traffic came. Downloads didn&rsquo;t. People were landing on the site, reading the feature list, and bouncing. Not because they didn&rsquo;t understand what ConfigMesh does, but because the offer itself wasn&rsquo;t landing.</p>
<p>After a month of feedback, conversations, and some honest self-reflection, I shipped ConfigMesh 1.1. This post is about the two assumptions I got wrong and what I changed.</p>
<h2 id="nobody-wants-to-pay-5month-for-text-files">Nobody wants to pay $5/month for text files</h2>
<p>I launched ConfigMesh at $5/month (or $50/year). From my side of the screen, that felt reasonable. I&rsquo;d built encryption infrastructure, a sync server, a native macOS app, months of development. Five bucks a month for all of that? Seemed fair.</p>
<p>From the user&rsquo;s side of the screen, the math looked very different. Dotfiles are tiny. We&rsquo;re talking kilobytes. A typical user&rsquo;s entire config collection is smaller than a single iPhone photo. Paying $5/month to sync that felt disproportionate, regardless of the engineering underneath.</p>
<p>This is the indie dev pricing trap: you price based on what it cost you to build, not on what people are willing to pay. The perceived value of syncing text files has a hard ceiling, and $5/month was above it. It didn&rsquo;t matter that the encryption was best-in-class or that the sync engine was smart. The sticker price created friction before anyone got to experience any of that.</p>
<p>The clearest signal came from Reddit. I posted ConfigMesh on the Mac apps subreddit and the feedback split into two camps. Some people said the price was just too high for what it does. Others went further: they didn&rsquo;t want another subscription at all, regardless of price. That second group was the more interesting signal. It&rsquo;s not about the dollar amount, it&rsquo;s about the model.</p>
<p><strong>ConfigMesh 1.1 drops the price to $1.99/month or $19.99/year.</strong> That&rsquo;s a significant cut, but it better matches where users anchor the value of this kind of tool. More importantly, it&rsquo;s no longer the only option (more on that below).</p>
<h2 id="trust-isnt-a-technical-problem">Trust isn&rsquo;t a technical problem</h2>
<p>This was the more interesting lesson.</p>
<p>ConfigMesh uses end-to-end encryption with a key that only you hold. The server literally cannot read your data. From a cryptographic standpoint, it doesn&rsquo;t matter where the data is stored because it&rsquo;s indistinguishable from random noise without your key.</p>
<p>I assumed that would be enough. It wasn&rsquo;t.</p>
<p>The same Reddit thread made this clear. People weren&rsquo;t just pushing back on the price, they were pushing back on the storage model. When I mentioned I was working on a Dropbox backend, the interest shifted noticeably. That was the option people actually wanted. Not cheaper cloud storage, but their own storage.</p>
<p>This isn&rsquo;t irrational. It&rsquo;s how people think about sensitive data. Ownership and control are emotional, not just technical. &ldquo;Your data is encrypted on our servers&rdquo; will always feel different from &ldquo;your data is on your Dropbox,&rdquo; even when the former is arguably more secure. You can&rsquo;t engineer your way past that.</p>
<p>The deeper realization was that some users don&rsquo;t need managed cloud storage at all. They already have storage (Dropbox, iCloud, NAS). They don&rsquo;t want another cloud backend. They just want the sync engine.</p>
<h2 id="enter-byos-bring-your-own-storage">Enter BYOS (Bring Your Own Storage)</h2>
<p>ConfigMesh 1.1 adds a one-time purchase option with no included cloud storage. Instead, you sync against your own Dropbox.</p>
<p>
<img class="img-fluid" src="./provider-selector.png" alt="Selecting Dropbox as your storage provider" />

</p>
<p>The tradeoffs are real: Dropbox is slower than the managed backend and more error-prone. Sync conflicts are harder to resolve gracefully when you don&rsquo;t control the storage layer. It&rsquo;s a compromise, but it&rsquo;s the compromise users actually asked for.</p>
<p>And yes, your data on Dropbox is still fully encrypted. Here&rsquo;s what it actually looks like on disk: just encrypted blobs, completely unreadable without your key.</p>
<p>
<img class="img-fluid" src="./dropbox-blobs.png" alt="Encrypted blobs in Dropbox" />

</p>
<p>Here&rsquo;s how the tiers work now:</p>
<ul>
<li><strong>Subscription ($1.99/month or $19.99/year):</strong> Fully managed, encrypted cloud storage. Fastest sync, most reliable. Same as before, just cheaper.</li>
<li><strong>One-time purchase:</strong> No cloud storage included. Sync against your own Dropbox. Slower, but you own everything.</li>
</ul>
<p>If you&rsquo;re on the one-time purchase tier and try to use the managed ConfigMesh backend, you&rsquo;ll see a clear upgrade prompt instead of a confusing error.</p>
<p>
<img class="img-fluid" src="./upgrade-required.png" alt="Upgrade required dialog" />

</p>
<p>Dropbox is the first supported provider. More are coming.</p>
<h2 id="what-id-tell-myself-at-launch">What I&rsquo;d tell myself at launch</h2>
<p>Ship your pricing assumptions as cheaply as possible. I could have launched with a lower price and a free trial and validated willingness-to-pay before building conviction around $5/month.</p>
<p>And when users tell you they don&rsquo;t trust something, don&rsquo;t argue with their reasoning. Just give them an alternative they&rsquo;re comfortable with. The best feature you can build is sometimes just getting out of the way.</p>
<p>ConfigMesh 1.1 is available now at <a href="https://configmesh.app">configmesh.app</a>. If you tried it before and bounced on the price or the storage model, give it another look.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>How I code with AI in 2026 - The 4 Persona System</title>
      <link>https://david.coffee/agentic-coding-with-4-personas/</link>
      <pubDate>Sun, 15 Feb 2026 10:00:00 +0900</pubDate>
      
      <guid>https://david.coffee/agentic-coding-with-4-personas/</guid><media:content url="https://david.coffee/agentic-coding-with-4-personas/assets/banner.jpg" medium="image" />
          <enclosure url="https://david.coffee/agentic-coding-with-4-personas/assets/banner.jpg" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/agentic-coding-with-4-personas/assets/banner.jpg&#34; /&gt;&lt;/p&gt;&lt;p&gt;Remember my &lt;a href=&#34;https://david.coffee/vibe-coding-part-1/&#34;&gt;Vibe Coding Essentials&lt;/a&gt; and &lt;a href=&#34;https://david.coffee/vibe-coding-advanced/&#34;&gt;Vibe Coding Advanced&lt;/a&gt; posts from last year? A lot changed since then, AI coding is now the norm and tools like OpenCode or Claude Code became industry standard. The core idea is still orchestrator models driving development, but the workflow is less &amp;ldquo;vibes&amp;rdquo; and more strict task orchestration.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve refined this to work across different tools, but the biggest shift is how I structure responsibility. It is now a 4-persona system where I keep architecture and product decisions, and the agents handle execution loops.&lt;/p&gt;
&lt;p&gt;I also created a YouTube video on this workflow if you prefer a walk-through over a long read.&lt;/p&gt;
&lt;iframe width=&#34;560&#34; height=&#34;315&#34; src=&#34;https://www.youtube.com/embed/MOEgv91p9vQ&#34; title=&#34;4 Persona System&#34; frameborder=&#34;0&#34; allow=&#34;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&#34; referrerpolicy=&#34;strict-origin-when-cross-origin&#34; allowfullscreen&gt;&lt;/iframe&gt;
&lt;h2 id=&#34;the-4-persona-system&#34;&gt;The 4-Persona System&lt;/h2&gt;
&lt;h3 id=&#34;1-architect-the-planner&#34;&gt;1. Architect: The Planner&lt;/h3&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./assets/architect.jpg&#34; alt=&#34;Architect Agent&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Model:&lt;/strong&gt; Usually Gemini 3 Pro, but any thinking model works&lt;/p&gt;
&lt;p&gt;The Architect is the brain of the operation. I usually reach for Gemini 3 Pro here because of its ability to ingest massive context windows and its &amp;ldquo;thinking mode,&amp;rdquo; but honestly, any model with strong reasoning capabilities does the job. The key requirement is that it can reason through complex problems without getting distracted by implementation details.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve set it up with access to WebSearch and Perplexity through my &lt;a href=&#34;https://mcpnest.dev&#34;&gt;MCPNest&lt;/a&gt; service. This ensures that whether I&amp;rsquo;m using Codex, OpenCode, Gemini, or Zed, the toolset remains identical across the board.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Workflow&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The Architect&amp;rsquo;s sole purpose is to output a comprehensive implementation plan into a &lt;code&gt;plans/xxx.md&lt;/code&gt; file (or GitHub Issue). The process is iterative and collaborative. I start by feeding the model my raw requirements and context. It then drafts a plan, which I immediately hand off to another high-reasoning model (like &lt;code&gt;codex-xhigh&lt;/code&gt; or &lt;code&gt;gemini-3-pro&lt;/code&gt;) for a second opinion.&lt;/p&gt;
&lt;p&gt;I usually run multiple passes on the design doc with different thinking agents. One might catch edge cases in the data model, while another suggests a cleaner API contract. I don&amp;rsquo;t care how long the architect agent takes to &amp;ldquo;think&amp;rdquo;: latency is irrelevant because I&amp;rsquo;m usually running a few of these in the background while I&amp;rsquo;m doing other things. I layer my own feedback on top of their critiques, and we loop through this cycle a few times until the plan is bulletproof.&lt;/p&gt;
&lt;p&gt;Only when the plan is solid do we move forward.&lt;/p&gt;
&lt;p&gt;I moved the full Architect prompt and review-loop prompt into a gist to keep this post readable:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://gist.github.com/dvcrn/2d2e2a29708665cf51b478f9a29d2c92&#34;&gt;Full prompts gist&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Architect prompt excerpt (I have another version of this that outputs into GitHub issues):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;You are Architect Mode, an implementation planning subagent for OpenCode. You generate structured, actionable plans that junior engineers can execute without including full code or large diffs.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(...)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;How you work
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Planning-only (no execution): You MUST NOT modify files, run commands, commit, or open PRs.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Double-check intent: Even if the user asks for implementation, you still only produce a plan; you do not implement.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Context gathering: Use &lt;span style=&#34;color:#e6db74&#34;&gt;`read`&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;`list`&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;`glob`&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;`grep`&lt;/span&gt; to understand repo layout and relevant files.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Output only: Produce a single, concise, well-structured plan.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Prefer descriptions over code: Reference identifiers and paths inline with backticks.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; If asked to implement: Clearly state you will not make changes, then provide the plan and suggest switching to an implementation agent.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Formatting rules
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(...)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Required plan sections
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Summary
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; File Changes
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Implementation Steps
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Contracts &amp;amp; Data
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Risks &amp;amp; Assumptions
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Validation
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Rollback
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Follow-ups
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Quality bar
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; List exact files to touch, or explain how to find them.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Keep context minimal but sufficient for execution.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; No full implementation or diffs.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; State assumptions explicitly if repository context is incomplete.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Persistence rule
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Always write or update the plan in &lt;span style=&#34;color:#e6db74&#34;&gt;`plans/YYYYMM-&amp;lt;plan-name&amp;gt;.md`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Safety
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(...)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Example of a created plan (shortened):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;# Add Stats Page
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;## Goal
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;Add a new &lt;span style=&#34;color:#e6db74&#34;&gt;`/stats`&lt;/span&gt; page to provide users with insights into their Japanese usage and common mistakes.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;## Features
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;1.&lt;/span&gt; &lt;span style=&#34;font-weight:bold&#34;&gt;**Correction Type Distribution**&lt;/span&gt;: Donut/pie chart by correction type.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;2.&lt;/span&gt; &lt;span style=&#34;font-weight:bold&#34;&gt;**Drill-down View**&lt;/span&gt;: Click a type to show recent mistakes.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;## Implementation Plan
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;### 1. Backend: Update `Fixmyjp.Corrections`
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Add &lt;span style=&#34;color:#e6db74&#34;&gt;`get_correction_type_stats(user_id)`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Add &lt;span style=&#34;color:#e6db74&#34;&gt;`list_recent_corrections_by_type(user_id, type, limit)`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;### 2. Frontend: Create `FixmyjpWeb.StatsLive`
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Add &lt;span style=&#34;color:#e6db74&#34;&gt;`lib/fixmyjp_web/live/stats_live.ex`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Use &lt;span style=&#34;color:#e6db74&#34;&gt;`FixmyjpWeb.Auth`&lt;/span&gt; for auth
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; UI:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Chart (CSS/SVG with Tailwind)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Interactive legend
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Mistake list with &lt;span style=&#34;color:#e6db74&#34;&gt;`original`&lt;/span&gt; -&amp;gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`corrected`&lt;/span&gt; and explanation
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;### 3. Routing
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Add &lt;span style=&#34;color:#e6db74&#34;&gt;`live(&amp;#34;/stats&amp;#34;, StatsLive, :index)`&lt;/span&gt; inside authenticated scope
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;### 4. Navigation
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Add Stats link in app layout
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;## Technical Details
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;### Database Queries
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;(...)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;### UI Structure
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;(...)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;2-implementer-the-builder&#34;&gt;2. Implementer: The Builder&lt;/h3&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./assets/builder1.jpg&#34; alt=&#34;Builder Agent&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Model:&lt;/strong&gt; Usually not a very-high thinking model: &lt;code&gt;codex-high&lt;/code&gt;, &lt;code&gt;codex-medium&lt;/code&gt;, or &lt;code&gt;Sonnet 4.5&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Once the plan is set, the Implementer takes over. I explicitly avoid the heavy thinking models here (like Claude 4.5 Opus) because the hard thinking has already been done by the Architect. The Implementer doesn&amp;rsquo;t need to be a genius; it just needs to be obedient.&lt;/p&gt;
&lt;p&gt;Its job is straightforward: follow the &lt;code&gt;plans/xxx.md&lt;/code&gt; file. It writes the code, runs my verification steps (linting, formatting, testing, compiling), and handles the git choreography of creating branches, committing changes, and opening PRs.&lt;/p&gt;
&lt;p&gt;I often offload this to cloud agents like Jules, Copilot or Codex, for example with a command like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cat plans/202601-deployment-last-used-tracking.md | jules remote new --repo dvcrn/mcpnest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./assets/image-20260215105458302.png&#34; alt=&#34;Jules Screenshot&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;The beauty is that the execution environment does not matter, local terminal or cloud container, as long as the plan is followed. Since all environments are configured identically, I can swap them out interchangeably. I can create a GitHub issue and assign it to &lt;em&gt;Copilot&lt;/em&gt;, but I can also open a terminal on my server and give it to &lt;em&gt;Claude Code&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id=&#34;3-review-agents-the-critics&#34;&gt;3. Review Agents: The Critics&lt;/h3&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./assets/reviewer.jpg&#34; alt=&#34;Reviewer Agent&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Models:&lt;/strong&gt; Gemini, Codex, Copilot in the cloud&lt;/p&gt;
&lt;p&gt;This is where I try to avoid the echo chamber. If a single model writes the code and reviews it, it&amp;rsquo;s likely to miss its own blind spots. That&amp;rsquo;s why I have Gemini and Codex automatically review every PR that gets created.&lt;/p&gt;
&lt;p&gt;For heavier or more critical changes, I throw Copilot into the mix as well. This gives me 3 independent agents reviewing the code, drastically reducing the chance of model bias slipping through.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./assets/image-20260215105712586.png&#34; alt=&#34;Gemini Code Review&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./assets/image-20260215105751439.png&#34; alt=&#34;Codex Code Review&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;I also keep a review persona handy in OpenCode, which I can summon with a slash command whenever I need a quick sanity check on a specific snippet.&lt;/p&gt;
&lt;h3 id=&#34;4-final-touchups-the-resolver&#34;&gt;4. Final Touchups: The Resolver&lt;/h3&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./assets/builder2.jpg&#34; alt=&#34;Resolver Agent&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Model:&lt;/strong&gt; Codex (or whatever model I&amp;rsquo;m using for coding at that time)&lt;/p&gt;
&lt;p&gt;Finally, we have the Resolver. This isn&amp;rsquo;t a single step but a loop. Codex reads the feedback from the Review Agents, asks me which points I want to address, and then handles them. Once the changes are made, it automatically requests another review from the critics. This loop continues until all critical issues are resolved and the code is polished.&lt;/p&gt;
&lt;p&gt;For my local tools, I have this neatly encapsulated into commands or subagents, for example this slash command here that I can use in OpenCode, Claude Code, or Codex:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Use &lt;span style=&#34;color:#e6db74&#34;&gt;`gh`&lt;/span&gt; or available tools to fetch all GitHub pull request comments for the PR that merges the current branch into main or master.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Current repo:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;!&lt;span style=&#34;color:#e6db74&#34;&gt;`git remote -v`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Current branch:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;!&lt;span style=&#34;color:#e6db74&#34;&gt;`git branch -r --contains HEAD`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Current PRs:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;!&lt;span style=&#34;color:#e6db74&#34;&gt;`gh pr list`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Analyze them, check the files they mentioned and output a list for each of them, explaining if this is relevant and should be fixed or not
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Use the following command to get the comments:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;```
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&lt;/span&gt;gh api graphql -F owner=&amp;#39;&amp;lt;OWNER&amp;gt;&amp;#39; -F name=&amp;#39;&amp;lt;REPO&amp;gt;&amp;#39; -F number=&amp;lt;PR NUMBER&amp;gt; -f
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;query=&amp;#39;query($name: String!, $owner: String!, $number: Int!) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;repository(owner: $owner, name: $name) { pullRequest(number: $number) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;reviewThreads(last: 100) { nodes { isResolved path comments(first: 1) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;nodes { body line } } } } } } }&amp;#39; --jq &amp;#39;.data.repository.pullRequest.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;reviewThreads.nodes[] | select(.isResolved == false) | &amp;#34;File: \(.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;path)\nComment: \(.comments.nodes[0].body)\n---&amp;#34;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Then ask the user whether we should fix them.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;After fixing, mark each fixed comment as resolved using this command:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;```
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&lt;/span&gt;gh api graphql -F threadId=&amp;#34;PRRT_kwDOQDhXrc5oS98H&amp;#34; -f
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;query=&amp;#39;mutation($threadId: ID!) { resolveReviewThread(input: {threadId:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$threadId}) { thread { isResolved } } }&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;```&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Once the code is committed and pushed, ask the user whether we should re-request a review by commenting &amp;#34;/gemini review @codex review&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;built-for-parallelization&#34;&gt;Built for Parallelization&lt;/h2&gt;
&lt;p&gt;The real superpower of this system is not just code generation. It scales.&lt;/p&gt;
&lt;p&gt;Because the Architect only outputs markdown files in the &lt;code&gt;plans/&lt;/code&gt; folder, I can have multiple Architect agents running in parallel without ever hitting a merge conflict. I often have half a dozen unimplemented plans sitting in my folder, iterating on them while the Implementer is busy building something else. It shifts the bottleneck from my own attention span to total system throughput. I&amp;rsquo;ve stopped optimizing for how fast an agent responds and started optimizing for how many high-quality plans I can have maturing simultaneously.&lt;/p&gt;
&lt;p&gt;The plan is the contract. Once it&amp;rsquo;s written, it doesn&amp;rsquo;t matter which agent picks it up or when.&lt;/p&gt;
&lt;h2 id=&#34;agents-control-agents&#34;&gt;Agents Control Agents&lt;/h2&gt;
&lt;p&gt;I mentioned &lt;a href=&#34;https://david.coffee/vibe-coding-advanced/&#34;&gt;sub-agents in my past posts&lt;/a&gt;, and they are the glue that holds this together. My agents are empowered to call other agents. The Implementer can spawn a code-review sub-agent to check its own work before committing. It can call the &lt;code&gt;commit&lt;/code&gt; agent to handle the git history. Each agent has it&amp;rsquo;s own responsibility and context, not all agents need to know everything and this is how I split knowledge.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a hierarchy where agents review other agents, and corrections are implemented by yet other agents.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;So what do I do?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I focus on the remaining 10%:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Making high-level architecture decisions&lt;/li&gt;
&lt;li&gt;Reviewing the final code quality&lt;/li&gt;
&lt;li&gt;Providing specific constraints (e.g., &amp;ldquo;use this package,&amp;rdquo; &amp;ldquo;structure it this way&amp;rdquo;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The agents handle the other 90%. It is not &amp;ldquo;AI writes all my code.&amp;rdquo; It is &amp;ldquo;AI handles boilerplate, tests, reviews, and iteration, while I focus on product and architecture.&amp;rdquo;&lt;/p&gt;
&lt;h2 id=&#34;where-this-still-fails&#34;&gt;Where This Still Fails&lt;/h2&gt;
&lt;p&gt;This setup is fast, but it still breaks in predictable ways:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Plan drift:&lt;/strong&gt; Implementers sometimes follow the spirit of a plan, not the exact constraints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;False confidence:&lt;/strong&gt; Multiple model approvals can still miss the same blind spot.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review noise:&lt;/strong&gt; Automated review threads can create churn if severity is not filtered.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost and latency:&lt;/strong&gt; Parallel agents are great until you look at spend and queue time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Over-planning risk:&lt;/strong&gt; Architect mode can over-spec work that should have been a quick spike.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;None of this is magic. It is just a tighter process with better delegation boundaries.&lt;/p&gt;
&lt;h2 id=&#34;what-changed-since-vibe-coding&#34;&gt;What Changed Since &amp;ldquo;Vibe Coding&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;The core orchestrator approach is still the same, but the maturity of the system has leveled up:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Clearer personas:&lt;/strong&gt; Each model has a specific job, not just &amp;ldquo;the AI&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better parallelization:&lt;/strong&gt; Work is decoupled via Plans, avoiding conflicts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More automation:&lt;/strong&gt; Review loops happen automatically without me babysitting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tool parity:&lt;/strong&gt; My MCP setup is identical across every tool I use.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloud flexibility:&lt;/strong&gt; Implementation is commoditized; I can use any cloud agent.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The result is that I can maintain 3-4 projects simultaneously, with agents handling the heavy lifting while I steer the ship.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Questions or want to share your AI coding setup? Let me know on &lt;a href=&#34;https://x.com/dvcrn&#34;&gt;Twitter/X&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/agentic-coding-with-4-personas/assets/banner.jpg" /></p><p>Remember my <a href="https://david.coffee/vibe-coding-part-1/">Vibe Coding Essentials</a> and <a href="https://david.coffee/vibe-coding-advanced/">Vibe Coding Advanced</a> posts from last year? A lot changed since then, AI coding is now the norm and tools like OpenCode or Claude Code became industry standard. The core idea is still orchestrator models driving development, but the workflow is less &ldquo;vibes&rdquo; and more strict task orchestration.</p>
<p>I&rsquo;ve refined this to work across different tools, but the biggest shift is how I structure responsibility. It is now a 4-persona system where I keep architecture and product decisions, and the agents handle execution loops.</p>
<p>I also created a YouTube video on this workflow if you prefer a walk-through over a long read.</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/MOEgv91p9vQ" title="4 Persona System" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
<h2 id="the-4-persona-system">The 4-Persona System</h2>
<h3 id="1-architect-the-planner">1. Architect: The Planner</h3>
<p>
<img class="img-fluid" src="./assets/architect.jpg" alt="Architect Agent" />

</p>
<p><strong>Model:</strong> Usually Gemini 3 Pro, but any thinking model works</p>
<p>The Architect is the brain of the operation. I usually reach for Gemini 3 Pro here because of its ability to ingest massive context windows and its &ldquo;thinking mode,&rdquo; but honestly, any model with strong reasoning capabilities does the job. The key requirement is that it can reason through complex problems without getting distracted by implementation details.</p>
<p>I&rsquo;ve set it up with access to WebSearch and Perplexity through my <a href="https://mcpnest.dev">MCPNest</a> service. This ensures that whether I&rsquo;m using Codex, OpenCode, Gemini, or Zed, the toolset remains identical across the board.</p>
<p><strong>The Workflow</strong></p>
<p>The Architect&rsquo;s sole purpose is to output a comprehensive implementation plan into a <code>plans/xxx.md</code> file (or GitHub Issue). The process is iterative and collaborative. I start by feeding the model my raw requirements and context. It then drafts a plan, which I immediately hand off to another high-reasoning model (like <code>codex-xhigh</code> or <code>gemini-3-pro</code>) for a second opinion.</p>
<p>I usually run multiple passes on the design doc with different thinking agents. One might catch edge cases in the data model, while another suggests a cleaner API contract. I don&rsquo;t care how long the architect agent takes to &ldquo;think&rdquo;: latency is irrelevant because I&rsquo;m usually running a few of these in the background while I&rsquo;m doing other things. I layer my own feedback on top of their critiques, and we loop through this cycle a few times until the plan is bulletproof.</p>
<p>Only when the plan is solid do we move forward.</p>
<p>I moved the full Architect prompt and review-loop prompt into a gist to keep this post readable:</p>
<p><a href="https://gist.github.com/dvcrn/2d2e2a29708665cf51b478f9a29d2c92">Full prompts gist</a></p>
<p>Architect prompt excerpt (I have another version of this that outputs into GitHub issues):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span>You are Architect Mode, an implementation planning subagent for OpenCode. You generate structured, actionable plans that junior engineers can execute without including full code or large diffs.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>(...)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>How you work
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Planning-only (no execution): You MUST NOT modify files, run commands, commit, or open PRs.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Double-check intent: Even if the user asks for implementation, you still only produce a plan; you do not implement.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Context gathering: Use <span style="color:#e6db74">`read`</span>, <span style="color:#e6db74">`list`</span>, <span style="color:#e6db74">`glob`</span>, <span style="color:#e6db74">`grep`</span> to understand repo layout and relevant files.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Output only: Produce a single, concise, well-structured plan.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Prefer descriptions over code: Reference identifiers and paths inline with backticks.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> If asked to implement: Clearly state you will not make changes, then provide the plan and suggest switching to an implementation agent.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Formatting rules
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>(...)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Required plan sections
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Summary
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> File Changes
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Implementation Steps
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Contracts &amp; Data
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Risks &amp; Assumptions
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Validation
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Rollback
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Follow-ups
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Quality bar
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> List exact files to touch, or explain how to find them.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Keep context minimal but sufficient for execution.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> No full implementation or diffs.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> State assumptions explicitly if repository context is incomplete.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Persistence rule
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Always write or update the plan in <span style="color:#e6db74">`plans/YYYYMM-&lt;plan-name&gt;.md`</span>.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Safety
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>(...)
</span></span></code></pre></div><p>Example of a created plan (shortened):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span># Add Stats Page
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Goal
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>Add a new <span style="color:#e6db74">`/stats`</span> page to provide users with insights into their Japanese usage and common mistakes.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Features
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">1.</span> <span style="font-weight:bold">**Correction Type Distribution**</span>: Donut/pie chart by correction type.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">2.</span> <span style="font-weight:bold">**Drill-down View**</span>: Click a type to show recent mistakes.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Implementation Plan
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">### 1. Backend: Update `Fixmyjp.Corrections`
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> Add <span style="color:#e6db74">`get_correction_type_stats(user_id)`</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Add <span style="color:#e6db74">`list_recent_corrections_by_type(user_id, type, limit)`</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">### 2. Frontend: Create `FixmyjpWeb.StatsLive`
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> Add <span style="color:#e6db74">`lib/fixmyjp_web/live/stats_live.ex`</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Use <span style="color:#e6db74">`FixmyjpWeb.Auth`</span> for auth
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> UI:
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">-</span> Chart (CSS/SVG with Tailwind)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">-</span> Interactive legend
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">-</span> Mistake list with <span style="color:#e6db74">`original`</span> -&gt; <span style="color:#e6db74">`corrected`</span> and explanation
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">### 3. Routing
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> Add <span style="color:#e6db74">`live(&#34;/stats&#34;, StatsLive, :index)`</span> inside authenticated scope
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">### 4. Navigation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> Add Stats link in app layout
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Technical Details
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">### Database Queries
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>(...)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">### UI Structure
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>(...)
</span></span></code></pre></div><h3 id="2-implementer-the-builder">2. Implementer: The Builder</h3>
<p>
<img class="img-fluid" src="./assets/builder1.jpg" alt="Builder Agent" />

</p>
<p><strong>Model:</strong> Usually not a very-high thinking model: <code>codex-high</code>, <code>codex-medium</code>, or <code>Sonnet 4.5</code></p>
<p>Once the plan is set, the Implementer takes over. I explicitly avoid the heavy thinking models here (like Claude 4.5 Opus) because the hard thinking has already been done by the Architect. The Implementer doesn&rsquo;t need to be a genius; it just needs to be obedient.</p>
<p>Its job is straightforward: follow the <code>plans/xxx.md</code> file. It writes the code, runs my verification steps (linting, formatting, testing, compiling), and handles the git choreography of creating branches, committing changes, and opening PRs.</p>
<p>I often offload this to cloud agents like Jules, Copilot or Codex, for example with a command like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cat plans/202601-deployment-last-used-tracking.md | jules remote new --repo dvcrn/mcpnest
</span></span></code></pre></div><p>
<img class="img-fluid" src="./assets/image-20260215105458302.png" alt="Jules Screenshot" />

</p>
<p>The beauty is that the execution environment does not matter, local terminal or cloud container, as long as the plan is followed. Since all environments are configured identically, I can swap them out interchangeably. I can create a GitHub issue and assign it to <em>Copilot</em>, but I can also open a terminal on my server and give it to <em>Claude Code</em>.</p>
<h3 id="3-review-agents-the-critics">3. Review Agents: The Critics</h3>
<p>
<img class="img-fluid" src="./assets/reviewer.jpg" alt="Reviewer Agent" />

</p>
<p><strong>Models:</strong> Gemini, Codex, Copilot in the cloud</p>
<p>This is where I try to avoid the echo chamber. If a single model writes the code and reviews it, it&rsquo;s likely to miss its own blind spots. That&rsquo;s why I have Gemini and Codex automatically review every PR that gets created.</p>
<p>For heavier or more critical changes, I throw Copilot into the mix as well. This gives me 3 independent agents reviewing the code, drastically reducing the chance of model bias slipping through.</p>
<p>
<img class="img-fluid" src="./assets/image-20260215105712586.png" alt="Gemini Code Review" />

</p>
<p>
<img class="img-fluid" src="./assets/image-20260215105751439.png" alt="Codex Code Review" />

</p>
<p>I also keep a review persona handy in OpenCode, which I can summon with a slash command whenever I need a quick sanity check on a specific snippet.</p>
<h3 id="4-final-touchups-the-resolver">4. Final Touchups: The Resolver</h3>
<p>
<img class="img-fluid" src="./assets/builder2.jpg" alt="Resolver Agent" />

</p>
<p><strong>Model:</strong> Codex (or whatever model I&rsquo;m using for coding at that time)</p>
<p>Finally, we have the Resolver. This isn&rsquo;t a single step but a loop. Codex reads the feedback from the Review Agents, asks me which points I want to address, and then handles them. Once the changes are made, it automatically requests another review from the critics. This loop continues until all critical issues are resolved and the code is polished.</p>
<p>For my local tools, I have this neatly encapsulated into commands or subagents, for example this slash command here that I can use in OpenCode, Claude Code, or Codex:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span>Use <span style="color:#e6db74">`gh`</span> or available tools to fetch all GitHub pull request comments for the PR that merges the current branch into main or master.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Current repo:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>!<span style="color:#e6db74">`git remote -v`</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Current branch:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>!<span style="color:#e6db74">`git branch -r --contains HEAD`</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Current PRs:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>!<span style="color:#e6db74">`gh pr list`</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Analyze them, check the files they mentioned and output a list for each of them, explaining if this is relevant and should be fixed or not
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Use the following command to get the comments:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">```
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"></span>gh api graphql -F owner=&#39;&lt;OWNER&gt;&#39; -F name=&#39;&lt;REPO&gt;&#39; -F number=&lt;PR NUMBER&gt; -f
</span></span><span style="display:flex;"><span>query=&#39;query($name: String!, $owner: String!, $number: Int!) {
</span></span><span style="display:flex;"><span>repository(owner: $owner, name: $name) { pullRequest(number: $number) {
</span></span><span style="display:flex;"><span>reviewThreads(last: 100) { nodes { isResolved path comments(first: 1) {
</span></span><span style="display:flex;"><span>nodes { body line } } } } } } }&#39; --jq &#39;.data.repository.pullRequest.
</span></span><span style="display:flex;"><span>reviewThreads.nodes[] | select(.isResolved == false) | &#34;File: \(.
</span></span><span style="display:flex;"><span>path)\nComment: \(.comments.nodes[0].body)\n---&#34;&#39;
</span></span><span style="display:flex;"><span><span style="color:#e6db74">```</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Then ask the user whether we should fix them.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>After fixing, mark each fixed comment as resolved using this command:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">```
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"></span>gh api graphql -F threadId=&#34;PRRT_kwDOQDhXrc5oS98H&#34; -f
</span></span><span style="display:flex;"><span>query=&#39;mutation($threadId: ID!) { resolveReviewThread(input: {threadId:
</span></span><span style="display:flex;"><span>$threadId}) { thread { isResolved } } }&#39;
</span></span><span style="display:flex;"><span><span style="color:#e6db74">```</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Once the code is committed and pushed, ask the user whether we should re-request a review by commenting &#34;/gemini review @codex review&#34;
</span></span></code></pre></div><h2 id="built-for-parallelization">Built for Parallelization</h2>
<p>The real superpower of this system is not just code generation. It scales.</p>
<p>Because the Architect only outputs markdown files in the <code>plans/</code> folder, I can have multiple Architect agents running in parallel without ever hitting a merge conflict. I often have half a dozen unimplemented plans sitting in my folder, iterating on them while the Implementer is busy building something else. It shifts the bottleneck from my own attention span to total system throughput. I&rsquo;ve stopped optimizing for how fast an agent responds and started optimizing for how many high-quality plans I can have maturing simultaneously.</p>
<p>The plan is the contract. Once it&rsquo;s written, it doesn&rsquo;t matter which agent picks it up or when.</p>
<h2 id="agents-control-agents">Agents Control Agents</h2>
<p>I mentioned <a href="https://david.coffee/vibe-coding-advanced/">sub-agents in my past posts</a>, and they are the glue that holds this together. My agents are empowered to call other agents. The Implementer can spawn a code-review sub-agent to check its own work before committing. It can call the <code>commit</code> agent to handle the git history. Each agent has it&rsquo;s own responsibility and context, not all agents need to know everything and this is how I split knowledge.</p>
<p>It&rsquo;s a hierarchy where agents review other agents, and corrections are implemented by yet other agents.</p>
<p><strong>So what do I do?</strong></p>
<p>I focus on the remaining 10%:</p>
<ul>
<li>Making high-level architecture decisions</li>
<li>Reviewing the final code quality</li>
<li>Providing specific constraints (e.g., &ldquo;use this package,&rdquo; &ldquo;structure it this way&rdquo;)</li>
</ul>
<p>The agents handle the other 90%. It is not &ldquo;AI writes all my code.&rdquo; It is &ldquo;AI handles boilerplate, tests, reviews, and iteration, while I focus on product and architecture.&rdquo;</p>
<h2 id="where-this-still-fails">Where This Still Fails</h2>
<p>This setup is fast, but it still breaks in predictable ways:</p>
<ol>
<li><strong>Plan drift:</strong> Implementers sometimes follow the spirit of a plan, not the exact constraints.</li>
<li><strong>False confidence:</strong> Multiple model approvals can still miss the same blind spot.</li>
<li><strong>Review noise:</strong> Automated review threads can create churn if severity is not filtered.</li>
<li><strong>Cost and latency:</strong> Parallel agents are great until you look at spend and queue time.</li>
<li><strong>Over-planning risk:</strong> Architect mode can over-spec work that should have been a quick spike.</li>
</ol>
<p>None of this is magic. It is just a tighter process with better delegation boundaries.</p>
<h2 id="what-changed-since-vibe-coding">What Changed Since &ldquo;Vibe Coding&rdquo;</h2>
<p>The core orchestrator approach is still the same, but the maturity of the system has leveled up:</p>
<ol>
<li><strong>Clearer personas:</strong> Each model has a specific job, not just &ldquo;the AI&rdquo;.</li>
<li><strong>Better parallelization:</strong> Work is decoupled via Plans, avoiding conflicts.</li>
<li><strong>More automation:</strong> Review loops happen automatically without me babysitting.</li>
<li><strong>Tool parity:</strong> My MCP setup is identical across every tool I use.</li>
<li><strong>Cloud flexibility:</strong> Implementation is commoditized; I can use any cloud agent.</li>
</ol>
<p>The result is that I can maintain 3-4 projects simultaneously, with agents handling the heavy lifting while I steer the ship.</p>
<hr>
<p><em>Questions or want to share your AI coding setup? Let me know on <a href="https://x.com/dvcrn">Twitter/X</a>.</em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>My &#39;Move Fast&#39; Stack for 2026</title>
      <link>https://david.coffee/move-fast-stack-2026/</link>
      <pubDate>Thu, 05 Feb 2026 10:00:00 +0900</pubDate>
      
      <guid>https://david.coffee/move-fast-stack-2026/</guid><media:content url="https://david.coffee/move-fast-stack-2026/assets/banner.jpeg" medium="image" />
          <enclosure url="https://david.coffee/move-fast-stack-2026/assets/banner.jpeg" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/move-fast-stack-2026/assets/banner.jpeg&#34; /&gt;&lt;/p&gt;&lt;h1 id=&#34;my-move-fast-stack-for-2026&#34;&gt;My &amp;ldquo;Move Fast&amp;rdquo; Stack for 2026&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Moving fast in 2026 isn&amp;rsquo;t just about the code—it&amp;rsquo;s about picking the right tools and letting AI handle the grunt work. My current go-to: Golang or Elixir (depending on what I&amp;rsquo;m building), SQLite + Litestream, and a 3-agent pipeline for code quality.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;In my last post, I reflected on the &lt;a href=&#34;https://david.coffee/one-year-of-indie-hacking&#34;&gt;ups and downs of the past 14 months&lt;/a&gt;. One of the biggest takeaways? As an indie hacker, &lt;strong&gt;velocity is your only real edge.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;re competing against teams with actual budgets, so you have to be able to ship, pivot, and maintain 3-4 apps at the same time without burning out.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the stack—both physical and digital—that lets me move at that pace in 2026.&lt;/p&gt;
&lt;h2 id=&#34;the-stack&#34;&gt;The Stack&lt;/h2&gt;
&lt;p&gt;The goal here isn&amp;rsquo;t &amp;ldquo;the most scalable&amp;rdquo; or &amp;ldquo;the most trendy.&amp;rdquo; It&amp;rsquo;s what gets me from idea to landing page the fastest.&lt;/p&gt;
&lt;h3 id=&#34;server-stack-elixir-vs-golang&#34;&gt;Server Stack: Elixir vs Golang&lt;/h3&gt;
&lt;p&gt;I use both Elixir and Golang, and the choice depends on what I&amp;rsquo;m building.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Golang is my AI-friendly workhorse:&lt;/strong&gt; Go compiles to a single binary, which makes it perfect for anything that needs to run on iOS/Android, CLI tools, or simple servers. The static typing is a game-changer when working with AI agents. The compile → error → fix loop is incredibly tight. AI writes code, Go compiler catches type errors immediately, AI fixes them, repeat. No surprises at runtime. This makes it perfect for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Simple servers that need to be rock-solid&lt;/li&gt;
&lt;li&gt;Anything embedded in iOS/Android apps (single binary, easy to share across platforms)&lt;/li&gt;
&lt;li&gt;APIs and microservices where I need confidence that it won&amp;rsquo;t blow up in production&lt;/li&gt;
&lt;li&gt;Background workers and CLI tools&lt;/li&gt;
&lt;li&gt;NPM packages that thinly wrap around Golang binaries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Elixir for web apps:&lt;/strong&gt; Phoenix + LiveView is still unbeatable when I need a full web app with real-time features. The entire frontend is built-in, so I skip the whole React/API dance. Just write server-side code and the UI updates automatically. Perfect for consumer-facing web apps.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The decision tree:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Consumer-facing web app? → Elixir (fully-featured package, LiveView gets you there fast, easy to extend)&lt;/li&gt;
&lt;li&gt;Starting with a server/service, or planning to ship to desktop/mobile? → Golang (start with CLI, embed later)&lt;/li&gt;
&lt;li&gt;Simple API or background worker? → Golang (compile-time safety)&lt;/li&gt;
&lt;li&gt;Need real-time features or distributed systems magic? → Elixir (OTP + LiveView)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The power of OTP (when I use Elixir):&lt;/strong&gt; When I do reach for Elixir and need to scale, OTP lets me do it within the same app. Need a queue and worker? Just spin up a GenServer process with &lt;strong&gt;Oban&lt;/strong&gt;. Need a cache? That&amp;rsquo;s another GenServer. Need a cron or periodic task? Another lightweight process. I don&amp;rsquo;t need to reach for Redis or external infra for every little thing. (I &lt;a href=&#34;https://david.coffee/mini-chatgpt-in-elixir-and-genserver&#34;&gt;wrote about this before&lt;/a&gt; if you want to see how powerful this is)&lt;/p&gt;
&lt;h3 id=&#34;databases&#34;&gt;Databases&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;SQLite + Litestream:&lt;/strong&gt; For quick projects, I start with SQLite and &lt;strong&gt;Litestream&lt;/strong&gt;. It&amp;rsquo;s a database replication and backup layer that sits on top of SQLite and streams changes to S3. No need to manage a &amp;ldquo;real&amp;rdquo; database cluster for small-to-medium apps. Every new project starts with this stack to not have additional database cost.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PlanetScale:&lt;/strong&gt; For anything that actually needs a hosted HA database, PlanetScale is the choice. It starts at $5, and after trying a few providers, this was easily the nicest to work with.&lt;/p&gt;
&lt;h3 id=&#34;web--apps&#34;&gt;Web &amp;amp; Apps&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Web:&lt;/strong&gt; &lt;strong&gt;Elixir with AlpineJS&lt;/strong&gt; (LiveView) for most things. But if I just need a simple JavaScript site without server-side logic, or something that purely consumes an API, I&amp;rsquo;ll use &lt;strong&gt;React + react-router&lt;/strong&gt; over Next.js. Pure JS apps are always in React for me - no need for the Next.js overhead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Apps:&lt;/strong&gt; My current favorite pattern is &lt;strong&gt;Embedded Golang in Swift&lt;/strong&gt;. I get to write the core logic in a language I love (Go) and wrap it in a native, buttery-smooth iOS UI. Best of both worlds. If I decide to build an Android app as well, I can use the same business logic in Golang and embed it into my Android app.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Auth:&lt;/strong&gt; &lt;strong&gt;Firebase Auth.&lt;/strong&gt; Don&amp;rsquo;t build your own auth. Just don&amp;rsquo;t. I&amp;rsquo;ve seen too many people waste weeks on this. Firebase has a great free tier and so far I’ve not once exceeded it. Supabase is a close second, but the cost per project was too much for me.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Subscriptions:&lt;/strong&gt; &lt;strong&gt;RevenueCat.&lt;/strong&gt; Much like auth, building your own subscription backend is a trap. RevenueCat handles everything related to payments, subscriptions, and entitlements. I interface with it as my single source of truth for whether a user is allowed to access a feature. It&amp;rsquo;s the only way to ship IAP in a weekend instead of a month.&lt;/p&gt;
&lt;h2 id=&#34;ai-workflow-the-4-agent-pipeline&#34;&gt;AI Workflow: The 4-Agent Pipeline&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s where things get interesting. I don&amp;rsquo;t just use AI for autocomplete—I use a sophisticated &lt;strong&gt;4-agent pipeline&lt;/strong&gt; to handle most of the grunt work. I wrote a deep dive on this in my post &lt;a href=&#34;https://david.coffee/agentic-coding-with-4-personas/&#34;&gt;How I Actually Code with AI in 2027&lt;/a&gt;, but here is the gist.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My repos are built for AI agents.&lt;/strong&gt; I maintain &lt;code&gt;RULES&lt;/code&gt;, &lt;code&gt;CONVENTIONS&lt;/code&gt;, and &lt;code&gt;AGENTS.md&lt;/code&gt; / &lt;code&gt;CLAUDE.md&lt;/code&gt; files that explain how the codebase works. But I don&amp;rsquo;t rely on the AI to follow rules perfectly. Just like I prefer compiled languages that catch errors at compile-time, I use &lt;strong&gt;pre-commit hooks as a static guarantee&lt;/strong&gt;. The AI can write whatever it wants, but it won&amp;rsquo;t get committed unless it passes formatting, linting, tests, type checking, and build validation.&lt;/p&gt;
&lt;p&gt;The pipeline:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Architect:&lt;/strong&gt; A high-reasoning model (Gemini 3 Pro) creates a detailed implementation plan.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Implementer:&lt;/strong&gt; A builder model (Codex/Claude) follows the plan and writes the code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reviewers:&lt;/strong&gt; Independent agents (Gemini, Codex, Copilot) review the PR for bugs and logic errors.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resolver:&lt;/strong&gt; An agent fixes the issues raised by reviewers until the code is polished.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Cloud agents for clear tasks:&lt;/strong&gt; Once I have a design doc ready, I pass it to a cloud agent (Codex, Copilot, Jules) that follows the plan while I work on something else.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Oracle server for exploratory work:&lt;/strong&gt; I run agents on Oracle&amp;rsquo;s free tier for the messy, long-running tasks that aren&amp;rsquo;t clearly defined yet. It&amp;rsquo;s like having a second engineer working on stuff while I do something else. I can SSH in from anywhere - even my phone. I keep my dev server in sync with my macOS system through &lt;a href=&#34;https://configmesh.app&#34;&gt;ConfigMesh&lt;/a&gt; which e2ee syncs dotfiles frictionless.&lt;/p&gt;
&lt;p&gt;By the time code reaches me, I&amp;rsquo;m only focusing on the final 10% - architecture decisions, product direction, and tricky edge cases. The agents handle the boilerplate, testing, and reviews.&lt;/p&gt;
&lt;p&gt;This is what lets me maintain 3-4 projects simultaneously while traveling.&lt;/p&gt;
&lt;h2 id=&#34;hosting-built-for-nomads&#34;&gt;Hosting: Built for Nomads&lt;/h2&gt;
&lt;p&gt;This is where I optimize for speed, cost, and the ability to deploy from anywhere in the world.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Phase 1 - Hetzner + Kubernetes:&lt;/strong&gt; Most of my small experiments start here. I run a self-managed k8s cluster on Hetzner where I can quickly deploy stuff from wherever I am without incurring additional cost per project. It&amp;rsquo;s my &amp;ldquo;testing ground&amp;rdquo; - if something shows promise, I move it to proper hosting. If it fails, I just delete the namespace. No extra bills or cost. I love how nice k8s is once you learn the basics.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Phase 2 - Fly.io:&lt;/strong&gt; Once a project graduates from experiments to &amp;ldquo;real thing,&amp;rdquo; it goes on Fly.io. I can quickly scale to the edge, and when I&amp;rsquo;m using PlanetScale, I can deploy close to the database region I&amp;rsquo;m in. Latency matters. The CLI is also dead simple to use from anywhere - even from my phone if I really need to.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cloudflare Workers:&lt;/strong&gt; All my React + react-router stuff lives here. Static sites, edge functions, anything that doesn&amp;rsquo;t need a backend. Fast, cheap, globally distributed. Perfect for when I&amp;rsquo;m traveling and need something to just work. Seriously, &lt;strong&gt;I love Cloudflare&lt;/strong&gt;. Using it brings me joy, it’s that nice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PlanetScale + Fly.io combo:&lt;/strong&gt; When SQLite + Litestream isn&amp;rsquo;t enough and I need a real hosted database, I use PlanetScale. I can deploy Fly.io apps close to the PlanetScale region to keep latency low.&lt;/p&gt;
&lt;h2 id=&#34;logistics-the-foundation--other-stuff-i-love&#34;&gt;Logistics: The Foundation &amp;amp; Other Stuff I love&lt;/h2&gt;
&lt;p&gt;Coding is 50% environment. If the logistics aren&amp;rsquo;t sorted, the &amp;ldquo;vibe&amp;rdquo; is off, and the code doesn&amp;rsquo;t flow. Everything in my stack is built around supporting a nomadic lifestyle - I need to be able to work from anywhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WeWork:&lt;/strong&gt; Even with all the corporate drama of years past, for a digital nomad it&amp;rsquo;s still the best &amp;ldquo;office-as-a-service.&amp;rdquo; Having a consistent, reliable place to work in almost any city keeps me grounded when I&amp;rsquo;m traveling. It&amp;rsquo;s my version of having &amp;ldquo;coworkers.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cloudflare Zero Trust:&lt;/strong&gt; This is the glue that ties everything together. Most of my work - whether on iPad or macOS - happens while connected to Zero Trust tunnels. It connects my infra, my projects, my servers, everything. Some services even connect to other infrastructure through private tunnels. Security is usually a speed bump, but Zero Trust makes it invisible. I can access all my internal tools and dev environments securely from anywhere without messing with VPNs. (I &lt;a href=&#34;https://david.coffee/cloudflare-zero-trust-tunnels&#34;&gt;wrote about this setup&lt;/a&gt; if you&amp;rsquo;re interested)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Noteplan:&lt;/strong&gt; My brain is a mess of tasks and markdown files. Noteplan is where they live. It&amp;rsquo;s simple, markdown-based, and doesn&amp;rsquo;t get in the way. Everything from writing ideas to project plans to work journals goes in here.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My Remarkable tablet:&lt;/strong&gt; As silly as that sounds, I haven’t found a better device that brings structure to everything I do. There’s something very Zen about sitting in a cafe with only a paper tablet and being forced to be semi-analog for once.&lt;/p&gt;
&lt;h2 id=&#34;whats-next&#34;&gt;What&amp;rsquo;s next?&lt;/h2&gt;
&lt;p&gt;The stack will probably change by 2027, but for now, this is what&amp;rsquo;s working. It&amp;rsquo;s about minimizing friction, maximizing &amp;ldquo;vibe,&amp;rdquo; and being able to work from anywhere in the world.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re building something, stop worrying about the &amp;ldquo;perfect&amp;rdquo; architecture and pick the tools that let you skip the boring stuff. For me, that&amp;rsquo;s Go when working with AI (tight feedback loops), Elixir when I need the magic (LiveView + OTP + easy scaling), a pipeline of AI agents to handle the grunt work and reviews, and infrastructure that I can access from my phone if needed.&lt;/p&gt;
&lt;p&gt;The goal isn&amp;rsquo;t to write more code - it&amp;rsquo;s to write less code that matters more, while agents handle the rest.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;What&amp;rsquo;s in your 2026 stack? Let me know on &lt;a href=&#34;https://x.com/dvcrn&#34;&gt;Twitter/X&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/move-fast-stack-2026/assets/banner.jpeg" /></p><h1 id="my-move-fast-stack-for-2026">My &ldquo;Move Fast&rdquo; Stack for 2026</h1>
<p><strong>TL;DR:</strong> Moving fast in 2026 isn&rsquo;t just about the code—it&rsquo;s about picking the right tools and letting AI handle the grunt work. My current go-to: Golang or Elixir (depending on what I&rsquo;m building), SQLite + Litestream, and a 3-agent pipeline for code quality.</p>
<hr>
<p>In my last post, I reflected on the <a href="https://david.coffee/one-year-of-indie-hacking">ups and downs of the past 14 months</a>. One of the biggest takeaways? As an indie hacker, <strong>velocity is your only real edge.</strong></p>
<p>You&rsquo;re competing against teams with actual budgets, so you have to be able to ship, pivot, and maintain 3-4 apps at the same time without burning out.</p>
<p>Here&rsquo;s the stack—both physical and digital—that lets me move at that pace in 2026.</p>
<h2 id="the-stack">The Stack</h2>
<p>The goal here isn&rsquo;t &ldquo;the most scalable&rdquo; or &ldquo;the most trendy.&rdquo; It&rsquo;s what gets me from idea to landing page the fastest.</p>
<h3 id="server-stack-elixir-vs-golang">Server Stack: Elixir vs Golang</h3>
<p>I use both Elixir and Golang, and the choice depends on what I&rsquo;m building.</p>
<p><strong>Golang is my AI-friendly workhorse:</strong> Go compiles to a single binary, which makes it perfect for anything that needs to run on iOS/Android, CLI tools, or simple servers. The static typing is a game-changer when working with AI agents. The compile → error → fix loop is incredibly tight. AI writes code, Go compiler catches type errors immediately, AI fixes them, repeat. No surprises at runtime. This makes it perfect for:</p>
<ul>
<li>Simple servers that need to be rock-solid</li>
<li>Anything embedded in iOS/Android apps (single binary, easy to share across platforms)</li>
<li>APIs and microservices where I need confidence that it won&rsquo;t blow up in production</li>
<li>Background workers and CLI tools</li>
<li>NPM packages that thinly wrap around Golang binaries</li>
</ul>
<p><strong>Elixir for web apps:</strong> Phoenix + LiveView is still unbeatable when I need a full web app with real-time features. The entire frontend is built-in, so I skip the whole React/API dance. Just write server-side code and the UI updates automatically. Perfect for consumer-facing web apps.</p>
<p><strong>The decision tree:</strong></p>
<ul>
<li>Consumer-facing web app? → Elixir (fully-featured package, LiveView gets you there fast, easy to extend)</li>
<li>Starting with a server/service, or planning to ship to desktop/mobile? → Golang (start with CLI, embed later)</li>
<li>Simple API or background worker? → Golang (compile-time safety)</li>
<li>Need real-time features or distributed systems magic? → Elixir (OTP + LiveView)</li>
</ul>
<p><strong>The power of OTP (when I use Elixir):</strong> When I do reach for Elixir and need to scale, OTP lets me do it within the same app. Need a queue and worker? Just spin up a GenServer process with <strong>Oban</strong>. Need a cache? That&rsquo;s another GenServer. Need a cron or periodic task? Another lightweight process. I don&rsquo;t need to reach for Redis or external infra for every little thing. (I <a href="https://david.coffee/mini-chatgpt-in-elixir-and-genserver">wrote about this before</a> if you want to see how powerful this is)</p>
<h3 id="databases">Databases</h3>
<p><strong>SQLite + Litestream:</strong> For quick projects, I start with SQLite and <strong>Litestream</strong>. It&rsquo;s a database replication and backup layer that sits on top of SQLite and streams changes to S3. No need to manage a &ldquo;real&rdquo; database cluster for small-to-medium apps. Every new project starts with this stack to not have additional database cost.</p>
<p><strong>PlanetScale:</strong> For anything that actually needs a hosted HA database, PlanetScale is the choice. It starts at $5, and after trying a few providers, this was easily the nicest to work with.</p>
<h3 id="web--apps">Web &amp; Apps</h3>
<p><strong>Web:</strong> <strong>Elixir with AlpineJS</strong> (LiveView) for most things. But if I just need a simple JavaScript site without server-side logic, or something that purely consumes an API, I&rsquo;ll use <strong>React + react-router</strong> over Next.js. Pure JS apps are always in React for me - no need for the Next.js overhead.</p>
<p><strong>Apps:</strong> My current favorite pattern is <strong>Embedded Golang in Swift</strong>. I get to write the core logic in a language I love (Go) and wrap it in a native, buttery-smooth iOS UI. Best of both worlds. If I decide to build an Android app as well, I can use the same business logic in Golang and embed it into my Android app.</p>
<p><strong>Auth:</strong> <strong>Firebase Auth.</strong> Don&rsquo;t build your own auth. Just don&rsquo;t. I&rsquo;ve seen too many people waste weeks on this. Firebase has a great free tier and so far I’ve not once exceeded it. Supabase is a close second, but the cost per project was too much for me.</p>
<p><strong>Subscriptions:</strong> <strong>RevenueCat.</strong> Much like auth, building your own subscription backend is a trap. RevenueCat handles everything related to payments, subscriptions, and entitlements. I interface with it as my single source of truth for whether a user is allowed to access a feature. It&rsquo;s the only way to ship IAP in a weekend instead of a month.</p>
<h2 id="ai-workflow-the-4-agent-pipeline">AI Workflow: The 4-Agent Pipeline</h2>
<p>Here&rsquo;s where things get interesting. I don&rsquo;t just use AI for autocomplete—I use a sophisticated <strong>4-agent pipeline</strong> to handle most of the grunt work. I wrote a deep dive on this in my post <a href="https://david.coffee/agentic-coding-with-4-personas/">How I Actually Code with AI in 2027</a>, but here is the gist.</p>
<p><strong>My repos are built for AI agents.</strong> I maintain <code>RULES</code>, <code>CONVENTIONS</code>, and <code>AGENTS.md</code> / <code>CLAUDE.md</code> files that explain how the codebase works. But I don&rsquo;t rely on the AI to follow rules perfectly. Just like I prefer compiled languages that catch errors at compile-time, I use <strong>pre-commit hooks as a static guarantee</strong>. The AI can write whatever it wants, but it won&rsquo;t get committed unless it passes formatting, linting, tests, type checking, and build validation.</p>
<p>The pipeline:</p>
<ol>
<li><strong>Architect:</strong> A high-reasoning model (Gemini 3 Pro) creates a detailed implementation plan.</li>
<li><strong>Implementer:</strong> A builder model (Codex/Claude) follows the plan and writes the code.</li>
<li><strong>Reviewers:</strong> Independent agents (Gemini, Codex, Copilot) review the PR for bugs and logic errors.</li>
<li><strong>Resolver:</strong> An agent fixes the issues raised by reviewers until the code is polished.</li>
</ol>
<p><strong>Cloud agents for clear tasks:</strong> Once I have a design doc ready, I pass it to a cloud agent (Codex, Copilot, Jules) that follows the plan while I work on something else.</p>
<p><strong>Oracle server for exploratory work:</strong> I run agents on Oracle&rsquo;s free tier for the messy, long-running tasks that aren&rsquo;t clearly defined yet. It&rsquo;s like having a second engineer working on stuff while I do something else. I can SSH in from anywhere - even my phone. I keep my dev server in sync with my macOS system through <a href="https://configmesh.app">ConfigMesh</a> which e2ee syncs dotfiles frictionless.</p>
<p>By the time code reaches me, I&rsquo;m only focusing on the final 10% - architecture decisions, product direction, and tricky edge cases. The agents handle the boilerplate, testing, and reviews.</p>
<p>This is what lets me maintain 3-4 projects simultaneously while traveling.</p>
<h2 id="hosting-built-for-nomads">Hosting: Built for Nomads</h2>
<p>This is where I optimize for speed, cost, and the ability to deploy from anywhere in the world.</p>
<p><strong>Phase 1 - Hetzner + Kubernetes:</strong> Most of my small experiments start here. I run a self-managed k8s cluster on Hetzner where I can quickly deploy stuff from wherever I am without incurring additional cost per project. It&rsquo;s my &ldquo;testing ground&rdquo; - if something shows promise, I move it to proper hosting. If it fails, I just delete the namespace. No extra bills or cost. I love how nice k8s is once you learn the basics.</p>
<p><strong>Phase 2 - Fly.io:</strong> Once a project graduates from experiments to &ldquo;real thing,&rdquo; it goes on Fly.io. I can quickly scale to the edge, and when I&rsquo;m using PlanetScale, I can deploy close to the database region I&rsquo;m in. Latency matters. The CLI is also dead simple to use from anywhere - even from my phone if I really need to.</p>
<p><strong>Cloudflare Workers:</strong> All my React + react-router stuff lives here. Static sites, edge functions, anything that doesn&rsquo;t need a backend. Fast, cheap, globally distributed. Perfect for when I&rsquo;m traveling and need something to just work. Seriously, <strong>I love Cloudflare</strong>. Using it brings me joy, it’s that nice.</p>
<p><strong>PlanetScale + Fly.io combo:</strong> When SQLite + Litestream isn&rsquo;t enough and I need a real hosted database, I use PlanetScale. I can deploy Fly.io apps close to the PlanetScale region to keep latency low.</p>
<h2 id="logistics-the-foundation--other-stuff-i-love">Logistics: The Foundation &amp; Other Stuff I love</h2>
<p>Coding is 50% environment. If the logistics aren&rsquo;t sorted, the &ldquo;vibe&rdquo; is off, and the code doesn&rsquo;t flow. Everything in my stack is built around supporting a nomadic lifestyle - I need to be able to work from anywhere.</p>
<p><strong>WeWork:</strong> Even with all the corporate drama of years past, for a digital nomad it&rsquo;s still the best &ldquo;office-as-a-service.&rdquo; Having a consistent, reliable place to work in almost any city keeps me grounded when I&rsquo;m traveling. It&rsquo;s my version of having &ldquo;coworkers.&rdquo;</p>
<p><strong>Cloudflare Zero Trust:</strong> This is the glue that ties everything together. Most of my work - whether on iPad or macOS - happens while connected to Zero Trust tunnels. It connects my infra, my projects, my servers, everything. Some services even connect to other infrastructure through private tunnels. Security is usually a speed bump, but Zero Trust makes it invisible. I can access all my internal tools and dev environments securely from anywhere without messing with VPNs. (I <a href="https://david.coffee/cloudflare-zero-trust-tunnels">wrote about this setup</a> if you&rsquo;re interested)</p>
<p><strong>Noteplan:</strong> My brain is a mess of tasks and markdown files. Noteplan is where they live. It&rsquo;s simple, markdown-based, and doesn&rsquo;t get in the way. Everything from writing ideas to project plans to work journals goes in here.</p>
<p><strong>My Remarkable tablet:</strong> As silly as that sounds, I haven’t found a better device that brings structure to everything I do. There’s something very Zen about sitting in a cafe with only a paper tablet and being forced to be semi-analog for once.</p>
<h2 id="whats-next">What&rsquo;s next?</h2>
<p>The stack will probably change by 2027, but for now, this is what&rsquo;s working. It&rsquo;s about minimizing friction, maximizing &ldquo;vibe,&rdquo; and being able to work from anywhere in the world.</p>
<p>If you&rsquo;re building something, stop worrying about the &ldquo;perfect&rdquo; architecture and pick the tools that let you skip the boring stuff. For me, that&rsquo;s Go when working with AI (tight feedback loops), Elixir when I need the magic (LiveView + OTP + easy scaling), a pipeline of AI agents to handle the grunt work and reviews, and infrastructure that I can access from my phone if needed.</p>
<p>The goal isn&rsquo;t to write more code - it&rsquo;s to write less code that matters more, while agents handle the rest.</p>
<hr>
<p><em>What&rsquo;s in your 2026 stack? Let me know on <a href="https://x.com/dvcrn">Twitter/X</a>.</em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>ConfigMesh - A modern and encrypted way to manage your dotfiles</title>
      <link>https://david.coffee/configmesh-release/</link>
      <pubDate>Mon, 26 Jan 2026 10:00:00 +0900</pubDate>
      
      <guid>https://david.coffee/configmesh-release/</guid><media:content url="https://david.coffee/configmesh-release/banner.png" medium="image" />
          <enclosure url="https://david.coffee/configmesh-release/banner.png" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/configmesh-release/banner.png&#34; /&gt;&lt;/p&gt;&lt;p&gt;I&amp;rsquo;m excited to announce the release of &lt;strong&gt;ConfigMesh&lt;/strong&gt;, a new macOS app I&amp;rsquo;ve been working on to solve a problem I&amp;rsquo;ve had for years: keeping my application settings and dotfiles in sync across multiple Macs.&lt;/p&gt;
&lt;p&gt;Check it out at &lt;a href=&#34;https://configmesh.app&#34;&gt;configmesh.app&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;the-problem-with-dotfiles&#34;&gt;The Problem with Dotfiles&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re a developer, you probably have a set of &amp;ldquo;dotfiles&amp;rdquo; - configuration files for your shell, editor, git, and other tools. Managing these has always been a bit of a chore.&lt;/p&gt;
&lt;p&gt;For a long time, I used a git repository and some symlinking scripts (like many of us do). It works, but it requires manual intervention. You have to remember to commit changes, push them, pull them on the other machine, and run the install script. If you forget, you end up with out-of-sync configs and weird behavior.&lt;/p&gt;
&lt;p&gt;And then there&amp;rsquo;s the issue of secrets. You can&amp;rsquo;t just commit your API keys or private tokens to a public (or even private) git repo without feeling a bit uneasy.&lt;/p&gt;
&lt;h2 id=&#34;enter-configmesh&#34;&gt;Enter ConfigMesh&lt;/h2&gt;
&lt;p&gt;ConfigMesh takes a different approach. It&amp;rsquo;s a native macOS app that runs in the background and watches your config files for changes. When a file changes, it automatically encrypts it and syncs it to your other machines.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./screenshot.png&#34; alt=&#34;ConfigMesh Screenshot&#34; /&gt;

&lt;/p&gt;
&lt;h2 id=&#34;key-features&#34;&gt;Key Features&lt;/h2&gt;
&lt;h3 id=&#34;-end-to-end-encryption&#34;&gt;🔒 End-to-End Encryption&lt;/h3&gt;
&lt;p&gt;This was a non-negotiable for me. ConfigMesh uses end-to-end encryption with a key that &lt;strong&gt;you&lt;/strong&gt; generate and own. The data is encrypted on your device before it ever leaves your machine. This means even if the sync server is compromised, your data is safe. Your key never leaves your device.&lt;/p&gt;
&lt;h3 id=&#34;-smart-syncing&#34;&gt;⚡️ Smart Syncing&lt;/h3&gt;
&lt;p&gt;The app intelligently diffs your config files. It doesn&amp;rsquo;t just blindly overwrite things; it checks for changes and only syncs files that have actually changed across machines, keeping everything in sync without you thinking about it.&lt;/p&gt;
&lt;h3 id=&#34;-granular-control&#34;&gt;🎯 Granular Control&lt;/h3&gt;
&lt;p&gt;You have full control over exactly what gets synced. ConfigMesh uses a powerful rules engine that lets you specify files and directories using standard glob patterns. Want to sync your entire &lt;code&gt;.config/nvim&lt;/code&gt; folder but exclude &lt;code&gt;lazy-lock.json&lt;/code&gt;? Easy.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./rules-ui.jpg&#34; alt=&#34;Rules Configuration&#34; /&gt;

&lt;/p&gt;
&lt;h3 id=&#34;-time-machine-for-configs&#34;&gt;🕰️ Time Machine for Configs&lt;/h3&gt;
&lt;p&gt;Ever messed up a config file and wished you could go back 10 minutes? ConfigMesh keeps a version history of your files. You can roll back to any previous version with a single click.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./revision-history.jpg&#34; alt=&#34;Revision History&#34; /&gt;

&lt;/p&gt;
&lt;h2 id=&#34;what-can-you-do-with-it&#34;&gt;What can you do with it?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Keep Vim settings in sync:&lt;/strong&gt; Have your perfect Neovim setup available on all your local machines and servers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backup AI Skills:&lt;/strong&gt; Store and sync your custom skills for OpenCode and Claude Code so your AI assistant is always ready.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;App Configurations:&lt;/strong&gt; Save and restore configurations for apps like Xcode, Mail, Terminal, and more. If you get a new Mac, just install ConfigMesh and your environment is ready.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;native--fast&#34;&gt;Native &amp;amp; Fast&lt;/h2&gt;
&lt;p&gt;ConfigMesh is built as a native macOS app. It&amp;rsquo;s designed to be lightweight and unobtrusive. It sits in your menu bar and does its job without getting in your way.&lt;/p&gt;
&lt;h2 id=&#34;roadmap&#34;&gt;Roadmap&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m just getting started. Here is what is coming next:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bring Your Own Storage:&lt;/strong&gt; Soon you&amp;rsquo;ll be able to pay a one-time fee and bring your own storage (starting with Dropbox).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server Support:&lt;/strong&gt; I&amp;rsquo;m wrapping up a Unix utility that will allow you to sync your files to headless servers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;try-it-out&#34;&gt;Try it out!&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been using ConfigMesh internally for a while now to keep my work and personal Macbooks in sync, and it&amp;rsquo;s been a game-changer. No more &amp;ldquo;wait, why is my alias not working?&amp;rdquo; moments.&lt;/p&gt;
&lt;p&gt;You can download it today at &lt;a href=&#34;https://configmesh.app&#34;&gt;configmesh.app&lt;/a&gt;. It currently comes with a &lt;strong&gt;$5/month plan&lt;/strong&gt; that includes fully managed, secure cloud storage.&lt;/p&gt;
&lt;p&gt;Let me know what you think!&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/configmesh-release/banner.png" /></p><p>I&rsquo;m excited to announce the release of <strong>ConfigMesh</strong>, a new macOS app I&rsquo;ve been working on to solve a problem I&rsquo;ve had for years: keeping my application settings and dotfiles in sync across multiple Macs.</p>
<p>Check it out at <a href="https://configmesh.app">configmesh.app</a>.</p>
<h2 id="the-problem-with-dotfiles">The Problem with Dotfiles</h2>
<p>If you&rsquo;re a developer, you probably have a set of &ldquo;dotfiles&rdquo; - configuration files for your shell, editor, git, and other tools. Managing these has always been a bit of a chore.</p>
<p>For a long time, I used a git repository and some symlinking scripts (like many of us do). It works, but it requires manual intervention. You have to remember to commit changes, push them, pull them on the other machine, and run the install script. If you forget, you end up with out-of-sync configs and weird behavior.</p>
<p>And then there&rsquo;s the issue of secrets. You can&rsquo;t just commit your API keys or private tokens to a public (or even private) git repo without feeling a bit uneasy.</p>
<h2 id="enter-configmesh">Enter ConfigMesh</h2>
<p>ConfigMesh takes a different approach. It&rsquo;s a native macOS app that runs in the background and watches your config files for changes. When a file changes, it automatically encrypts it and syncs it to your other machines.</p>
<p>
<img class="img-fluid" src="./screenshot.png" alt="ConfigMesh Screenshot" />

</p>
<h2 id="key-features">Key Features</h2>
<h3 id="-end-to-end-encryption">🔒 End-to-End Encryption</h3>
<p>This was a non-negotiable for me. ConfigMesh uses end-to-end encryption with a key that <strong>you</strong> generate and own. The data is encrypted on your device before it ever leaves your machine. This means even if the sync server is compromised, your data is safe. Your key never leaves your device.</p>
<h3 id="-smart-syncing">⚡️ Smart Syncing</h3>
<p>The app intelligently diffs your config files. It doesn&rsquo;t just blindly overwrite things; it checks for changes and only syncs files that have actually changed across machines, keeping everything in sync without you thinking about it.</p>
<h3 id="-granular-control">🎯 Granular Control</h3>
<p>You have full control over exactly what gets synced. ConfigMesh uses a powerful rules engine that lets you specify files and directories using standard glob patterns. Want to sync your entire <code>.config/nvim</code> folder but exclude <code>lazy-lock.json</code>? Easy.</p>
<p>
<img class="img-fluid" src="./rules-ui.jpg" alt="Rules Configuration" />

</p>
<h3 id="-time-machine-for-configs">🕰️ Time Machine for Configs</h3>
<p>Ever messed up a config file and wished you could go back 10 minutes? ConfigMesh keeps a version history of your files. You can roll back to any previous version with a single click.</p>
<p>
<img class="img-fluid" src="./revision-history.jpg" alt="Revision History" />

</p>
<h2 id="what-can-you-do-with-it">What can you do with it?</h2>
<ul>
<li><strong>Keep Vim settings in sync:</strong> Have your perfect Neovim setup available on all your local machines and servers.</li>
<li><strong>Backup AI Skills:</strong> Store and sync your custom skills for OpenCode and Claude Code so your AI assistant is always ready.</li>
<li><strong>App Configurations:</strong> Save and restore configurations for apps like Xcode, Mail, Terminal, and more. If you get a new Mac, just install ConfigMesh and your environment is ready.</li>
</ul>
<h2 id="native--fast">Native &amp; Fast</h2>
<p>ConfigMesh is built as a native macOS app. It&rsquo;s designed to be lightweight and unobtrusive. It sits in your menu bar and does its job without getting in your way.</p>
<h2 id="roadmap">Roadmap</h2>
<p>I&rsquo;m just getting started. Here is what is coming next:</p>
<ul>
<li><strong>Bring Your Own Storage:</strong> Soon you&rsquo;ll be able to pay a one-time fee and bring your own storage (starting with Dropbox).</li>
<li><strong>Server Support:</strong> I&rsquo;m wrapping up a Unix utility that will allow you to sync your files to headless servers.</li>
</ul>
<h2 id="try-it-out">Try it out!</h2>
<p>I&rsquo;ve been using ConfigMesh internally for a while now to keep my work and personal Macbooks in sync, and it&rsquo;s been a game-changer. No more &ldquo;wait, why is my alias not working?&rdquo; moments.</p>
<p>You can download it today at <a href="https://configmesh.app">configmesh.app</a>. It currently comes with a <strong>$5/month plan</strong> that includes fully managed, secure cloud storage.</p>
<p>Let me know what you think!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Reflecting on 14 months of indie hacking - Building 7 projects and learning hard lessons</title>
      <link>https://david.coffee/one-year-of-indie-hacking/</link>
      <pubDate>Sun, 11 Jan 2026 10:00:00 +0900</pubDate>
      
      <guid>https://david.coffee/one-year-of-indie-hacking/</guid><media:content url="https://david.coffee/one-year-of-indie-hacking/assets/banner.jpeg" medium="image" />
          <enclosure url="https://david.coffee/one-year-of-indie-hacking/assets/banner.jpeg" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/one-year-of-indie-hacking/assets/banner.jpeg&#34; /&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; 14 months ago, I quit my job to build indie products. Built 7 projects, currently at ~$45 MRR total across all projects, 900+ active users. Most projects failed financially, but learned valuable lessons about building for broad audiences over niche tools.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;A little over 14 months ago, I made the drastic decision to quit my full-time job of 6+ years as Engineering Manager at &lt;a href=&#34;https://mercari.com&#34;&gt;Mercari&lt;/a&gt; to &lt;em&gt;go my own path&lt;/em&gt; and find my own success.&lt;/p&gt;
&lt;p&gt;It wasn&amp;rsquo;t a rage-quit or a &amp;ldquo;burn it all down&amp;rdquo; moment. It was a calculated risk to see if I could build a life entirely on my own terms. I’d like to reflect a bit on the year, how it’s going and what I’ll be doing going forward.&lt;/p&gt;
&lt;h2 id=&#34;do-i-regret-my-decision&#34;&gt;Do I regret my decision?&lt;/h2&gt;
&lt;p&gt;No, but it’s hard. I already went into this with the mindset that the first 50 or so ideas will fail, and maybe if I’m lucky, just maybe, the 51st idea will make some money.&lt;/p&gt;
&lt;p&gt;Even with this mindset, actually failing over and over again is hard. We need small successes to understand whether what we’re doing is the correct thing, and being starved of wins is mentally taxing.&lt;/p&gt;
&lt;h2 id=&#34;what-i-worked-on-7-8-projects-over-14-months&#34;&gt;What I worked on: 7-8 projects over 14 months&lt;/h2&gt;
&lt;p&gt;The point of indie hacking is to move quick, try out small ideas, see what works and then focus more time or move on. Over the 14 months I worked on roughly 7-8 projects.&lt;/p&gt;
&lt;p&gt;Looking back, these projects fall into three distinct buckets, each teaching me a very different lesson.&lt;/p&gt;
&lt;h3 id=&#34;group-1-the-dev-tools-trap&#34;&gt;Group 1: The Dev Tools Trap&lt;/h3&gt;
&lt;p&gt;I started where I was most comfortable: building tools for other developers. This turned out to be the hardest path. The market is saturated, the users are demanding, and competing with free venture-backed tools is a nightmare.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Microfn (microFunction) - Your toolbox for tiny composable functions&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./assets/image-20260111123641516.png&#34; alt=&#34;Screenshot of Microfn&#34; /&gt;

&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What:&lt;/strong&gt; Cloud service, function runner and editor for small (micro), composable JavaScript/TypeScript functions. Hook them up to CRON, Webhooks, Agents over MCP or other automation platforms. It double serves as a dev environment for AI agents, to allow agents to quickly deploy code and use that code as new tools right away.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How it went:&lt;/strong&gt; 1 active user (myself). Fully functional and stable, but financially a failure. My partner lost interest and exited the project midway through, leaving me alone with it. No marketing has been done.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Feature-complete maintenance mode. It will stay in organic-growth-maintenance mode until usage picks up, with max 1 new development per week.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Lesson:&lt;/strong&gt; Building a dev service is hard; getting people to switch from their existing workflow (Vercel, AWS, etc.) is near impossible without a massive marketing budget. Ad keywords are always outbid by the bigger players (Cloudflare, Google, etc). The potential is there, but it needs it&amp;rsquo;s killer-feature moment.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;MCPNest - Host your MCP servers in the cloud&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./assets/image-20260111123549187.png&#34; alt=&#34;Screenshot of MCPNest&#34; /&gt;

&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What:&lt;/strong&gt; &lt;a href=&#34;https://mcpnest.dev&#34;&gt;https://mcpnest.dev&lt;/a&gt; - Cloud service for running MCP servers with focus on simplicity. This was an experiment built at the peak of the MCP servers craze. It greatly simplifies re-using sets of servers, for example to have a specific set of servers + tools for a dev persona, one for a casual persona.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How it went:&lt;/strong&gt; 14 WAU, currently pays for itself. Financially a failure. Active dev time roughly 1 week.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Active, low maintenance. As long as it pays for itself it will continue. It’s very low maintenance and does what it claims to do very well.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Lesson:&lt;/strong&gt; Even jumping on a &amp;ldquo;hot trend&amp;rdquo; (MCP) isn&amp;rsquo;t enough if the audience is too niche. The timing could should have been 1-2 months faster to really hit the peak of the MCP craze.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Mailwire - Smart Email Proxy&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./assets/image-20260111123706497.png&#34; alt=&#34;Screenshot of Mailwire&#34; /&gt;

&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What:&lt;/strong&gt; &lt;a href=&#34;https://maskwire.com&#34;&gt;Mailwire&lt;/a&gt; is a transparent email proxy for power users. This is not a new project and already alive for a couple years, although in maintenance mode. It sits in between the users email client and their email server, and adds power-user features like auto-stripping of sensitive information or rewriting email senders (&lt;a href=&#34;mailto:bank1@mydomain.com&#34;&gt;bank1@mydomain.com&lt;/a&gt;) to match the recipient address (&lt;a href=&#34;mailto:foo@bank.com&#34;&gt;foo@bank.com&lt;/a&gt;). Active development time was many many months a few years ago.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How it went&lt;/strong&gt;: 2 active users, a big failure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Low-energy maintenance mode. As I’m a user myself and need all of this, it will continue, but stay in maintenance and low-energy mode.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Lesson:&lt;/strong&gt; This app is just too niche. I built it for myself, and turns out, I&amp;rsquo;m the only one who really needs it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;group-2-the-steady-utilities&#34;&gt;Group 2: The Steady Utilities&lt;/h3&gt;
&lt;p&gt;While the ambitious platforms struggled, my small, simple utilities found a quiet, steady rhythm. These aren&amp;rsquo;t trying to change the world, just fix a small annoyance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Advanced AI Actions&lt;/strong&gt;&lt;/p&gt;
&lt;img src=&#34;./assets/image-20260111123917852.png&#34; alt=&#34;Screenshot of Advanced AI Actions&#34; style=&#34;zoom:50%;&#34; /&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What:&lt;/strong&gt; One-time purchase iOS utility app to expose LLM providers through Siri Shortcuts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How it went:&lt;/strong&gt; Around $15 MRR (amortized). Considering that it’s not a subscription app, I would say this is a success. I am glad people get value out of it. Active dev time was about a week, and then a few days here and there to fix bugs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Full support going forward. Maybe migrating it to a full free-but-with-unlock app and adding a paid provider option to see if people can be converted to monthly payments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Lesson:&lt;/strong&gt; People will pay for simple utilities that solve a specific problem on their device.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Masked Email Manager&lt;/strong&gt;&lt;/p&gt;
&lt;img src=&#34;./assets/image-20260111123903570.png&#34; alt=&#34;Screenshot of Masked Email Manager&#34; style=&#34;zoom:50%;&#34; /&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What:&lt;/strong&gt; Utility iOS app for managing Fastmail’s Masked Email feature. Already built this before I quit my job, but I’ve been adding more features such as Siri Shortcuts support to it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How it went:&lt;/strong&gt; Around 5-6 unlocks each month. I think that’s okay considering how niche it is, and being one-time purchase. Active development time of around 1 month in total.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Full support, will continue as one-time unlock utility app.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Lesson:&lt;/strong&gt; Niche utilities can work if they draft off a larger platform&amp;rsquo;s userbase (Fastmail).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;group-3-the-consumer-bets&#34;&gt;Group 3: The Consumer Bets&lt;/h3&gt;
&lt;p&gt;This is where I saw the most actual traction—solving problems for regular people, not just other coders.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix My Japanese&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./assets/image-20260111123753384.png&#34; alt=&#34;Screenshot of Fix My Japanese&#34; /&gt;

&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What:&lt;/strong&gt; Japanese grammar and sentence structure correction through AI, created back when the bleeding edge &lt;code&gt;davinci&lt;/code&gt; model was the smartest thing around. Comes as web service and iOS app that’s free with subscription.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How it went:&lt;/strong&gt; $14 MRR, 6 active subscriptions, 180 DAU, 700 WAU. Project is steadily growing but &lt;strong&gt;financially a failure&lt;/strong&gt;. Active dev time is a couple months at this point, started way back.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Active growth. As long as it pays for itself it will stay alive. I will continue to try and integrate more paid features to see if some of those 700 WAU can be converted to paid users.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Lesson:&lt;/strong&gt; Steadily growing but effectively a failure financially. It’s a great &lt;em&gt;free&lt;/em&gt; product, but getting people to pay for additional functionality is hard when the free tier is &amp;ldquo;good enough&amp;rdquo;. Nowadays people use ChatGPT which can do 80% of what Fix My Japanese does, further making it unlikely for people to pay.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Quickshot Photo Editor&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./assets/image-20260111123842155.png&#34; alt=&#34;Screenshot of Quickshot&#34; /&gt;

&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What:&lt;/strong&gt; &lt;a href=&#34;https://quickshot.photo&#34;&gt;Quickshot&lt;/a&gt; is an AI-powered photo editor for iPhone (Edit photos through words, not tools). It started when photo editing with AI models became better and viable. It’s using a clever pipeline of models and AI upscaling to make targeted edits without destroying the original photo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How it went:&lt;/strong&gt; 6 active subscriptions, $16 MRR. Financially a failure, but one of my apps with the most potential.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status:&lt;/strong&gt; Active development. This app has potential and I will put more time into it after my current focus is completed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Lesson:&lt;/strong&gt; Despite fewer updates, this made more money than the others. It has the most potential because it appeals to a broad consumer base, but nowadays also competes against ChatGPT which is a hard uphill battle.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;fun-stuff-the-playground&#34;&gt;Fun stuff (The Playground)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://esim.d.sh&#34;&gt;esimdb&lt;/a&gt;: E-Sim comparison site using providers I frequently use and vouch for.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://david.coffee/projects/desks/&#34;&gt;Desks for WeWork&lt;/a&gt;: Native iOS app for managing WeWork bookings.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://theenabler.app&#34;&gt;The Enabler&lt;/a&gt;: Companion that always agrees with you, no matter what.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;what-i-learned&#34;&gt;What I learned&lt;/h2&gt;
&lt;p&gt;But financials aren’t the only things I managed to learn. I learned a lot of things on the way that hopefully will help me keeping my focus going forward more sharp.&lt;/p&gt;
&lt;h3 id=&#34;1-always-build-with-the-monetization-strategy-in-mind&#34;&gt;1. Always build with the monetization strategy in mind&lt;/h3&gt;
&lt;p&gt;Very obvious first learning, but never build something without knowing how to monetize it. I am pretty good at keeping at that already, but some leftover apps (mainly Fix My Japanese) are fighting with the problem of converting free users to a paid tier.&lt;/p&gt;
&lt;h4 id=&#34;11-but-keep-a-free-tier-around-because-most-people-dont-want-to-pay-money&#34;&gt;1.1 But, keep a free tier around because most people don’t want to pay money&lt;/h4&gt;
&lt;p&gt;Getting &lt;em&gt;paid&lt;/em&gt; users is tough. There’s a lot of mental steps involved to getting someone to pay for your thing, but you’ll want those users as well for growth. Offer some kind of free tier that is heavily restricted in usage and clearly pushes to the upgrade path.&lt;/p&gt;
&lt;h3 id=&#34;2-saas-is-hard-and-competitive&#34;&gt;2. SaaS is hard and competitive&lt;/h3&gt;
&lt;p&gt;Microfn taught me that building dev tooling and hosting services is extremely hard. The field is highly competitive with the big players (like Cloudflare workers, Firebase, Google App Engine, etc) being hard to out-maneuver for user acquisition. Can’t run ads and outbid those companies, which leaves organic growth.&lt;/p&gt;
&lt;h3 id=&#34;3-most-of-my-stuff-is-too-niche-focus-on-broad-consumers-instead&#34;&gt;3. Most of my stuff is too niche, focus on broad consumers instead&lt;/h3&gt;
&lt;p&gt;Yeah hard truth. Mailwire is a great transparent email proxy, but who in the world besides me needs this? Microfn is a great platform, but most people don&amp;rsquo;t need this. MCP Nest does exactly what it ought out to do, but the audience is tiny.&lt;/p&gt;
&lt;p&gt;The tech world is a tight community, &lt;strong&gt;better to focus on broad consumer apps instead&lt;/strong&gt; (Quickshot, Fix My Japanese) where there&amp;rsquo;s more room for niche players and lower customer acquisition costs.&lt;/p&gt;
&lt;h3 id=&#34;4-you-need-luck&#34;&gt;4. You need luck&lt;/h3&gt;
&lt;p&gt;Some apps like Quickshot that haven’t received updates still made more money than a lot of other apps that actively received new versions. Even the best idea needs the right timing and the right bit of luck to grow.&lt;/p&gt;
&lt;h3 id=&#34;5-indie-hacking-is-lonely&#34;&gt;5. Indie hacking is lonely&lt;/h3&gt;
&lt;p&gt;Another hard truth: indie hacking and working on stuff alone can feel lonely fast.&lt;/p&gt;
&lt;p&gt;It’s a strange paradox. I have the ultimate freedom, I can travel anywhere, work from a café in Tokyo or a WeWork in Sydney, and take a random Tuesday off to go hiking. But that freedom disconnects you from the rhythm of the rest of the world.&lt;/p&gt;
&lt;p&gt;My friends work full-time jobs. They can&amp;rsquo;t do spontaneous trips. They have &amp;ldquo;work stories&amp;rdquo; and office drama that I can no longer relate to. There were times when I was so wired into my work, in a foreign city, that I forgot where I actually &lt;em&gt;was&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Having coworkers or a community is nice, and my closest equivalent to that are currently the WeWork offices I use for working across the globe. It&amp;rsquo;s a different kind of lonely, not sad, just&amp;hellip; singular.&lt;/p&gt;
&lt;h3 id=&#34;6-marketing-is-a-skill-i-desperately-need-to-learn&#34;&gt;6. Marketing is a skill I desperately need to learn&lt;/h3&gt;
&lt;p&gt;Building a great product is only half the battle—maybe even less. I’ve realized that I have a major blind spot when it comes to marketing. Some of the services I built are technically solid and solve real problems, but I have no idea how to get them in front of the right people.&lt;/p&gt;
&lt;p&gt;As a developer, it&amp;rsquo;s easy to fall into the &amp;ldquo;build it and they will come&amp;rdquo; trap. But they don&amp;rsquo;t. Marketing is a skill just like coding, and it&amp;rsquo;s one I need to get better at if I want any of these projects to actually succeed.&lt;/p&gt;
&lt;h2 id=&#34;going-forward&#34;&gt;Going forward&lt;/h2&gt;
&lt;p&gt;So what is next for me in 2026?&lt;/p&gt;
&lt;p&gt;I have no regrets and no plan to stop (for now), but I will focus on broader consumer apps and keep the dev-centric services in maintenance mode with minimal time put into them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Specific goals for 2026:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reach $500 MRR by end of year&lt;/li&gt;
&lt;li&gt;Focus on 2-3 core projects instead of spreading thin across 7+&lt;/li&gt;
&lt;li&gt;Be more data-driven: implement proper analytics with NewRelic and Posthog dashboards to see what works and what doesn&amp;rsquo;t&lt;/li&gt;
&lt;li&gt;Double down on Quickshot and Fix My Japanese - the projects with the most traction&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I may need to start winding down some things in 2026 to keep my focus tight. That still feels like shooting your baby, but as my focus becomes thinner, this will become necessary.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you&amp;rsquo;re interested in following my indie hacking journey, you can find me on &lt;a href=&#34;https://x.com/dvcrn&#34;&gt;Twitter/X&lt;/a&gt; or check out my projects at &lt;a href=&#34;https://david.coffee&#34;&gt;david.coffee&lt;/a&gt;. Always happy to chat with fellow indie hackers about the ups and downs of this journey.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/one-year-of-indie-hacking/assets/banner.jpeg" /></p><p><strong>TL;DR:</strong> 14 months ago, I quit my job to build indie products. Built 7 projects, currently at ~$45 MRR total across all projects, 900+ active users. Most projects failed financially, but learned valuable lessons about building for broad audiences over niche tools.</p>
<hr>
<p>A little over 14 months ago, I made the drastic decision to quit my full-time job of 6+ years as Engineering Manager at <a href="https://mercari.com">Mercari</a> to <em>go my own path</em> and find my own success.</p>
<p>It wasn&rsquo;t a rage-quit or a &ldquo;burn it all down&rdquo; moment. It was a calculated risk to see if I could build a life entirely on my own terms. I’d like to reflect a bit on the year, how it’s going and what I’ll be doing going forward.</p>
<h2 id="do-i-regret-my-decision">Do I regret my decision?</h2>
<p>No, but it’s hard. I already went into this with the mindset that the first 50 or so ideas will fail, and maybe if I’m lucky, just maybe, the 51st idea will make some money.</p>
<p>Even with this mindset, actually failing over and over again is hard. We need small successes to understand whether what we’re doing is the correct thing, and being starved of wins is mentally taxing.</p>
<h2 id="what-i-worked-on-7-8-projects-over-14-months">What I worked on: 7-8 projects over 14 months</h2>
<p>The point of indie hacking is to move quick, try out small ideas, see what works and then focus more time or move on. Over the 14 months I worked on roughly 7-8 projects.</p>
<p>Looking back, these projects fall into three distinct buckets, each teaching me a very different lesson.</p>
<h3 id="group-1-the-dev-tools-trap">Group 1: The Dev Tools Trap</h3>
<p>I started where I was most comfortable: building tools for other developers. This turned out to be the hardest path. The market is saturated, the users are demanding, and competing with free venture-backed tools is a nightmare.</p>
<p><strong>Microfn (microFunction) - Your toolbox for tiny composable functions</strong></p>
<p>
<img class="img-fluid" src="./assets/image-20260111123641516.png" alt="Screenshot of Microfn" />

</p>
<ul>
<li><strong>What:</strong> Cloud service, function runner and editor for small (micro), composable JavaScript/TypeScript functions. Hook them up to CRON, Webhooks, Agents over MCP or other automation platforms. It double serves as a dev environment for AI agents, to allow agents to quickly deploy code and use that code as new tools right away.</li>
<li><strong>How it went:</strong> 1 active user (myself). Fully functional and stable, but financially a failure. My partner lost interest and exited the project midway through, leaving me alone with it. No marketing has been done.</li>
<li><strong>Status:</strong> Feature-complete maintenance mode. It will stay in organic-growth-maintenance mode until usage picks up, with max 1 new development per week.</li>
<li><strong>The Lesson:</strong> Building a dev service is hard; getting people to switch from their existing workflow (Vercel, AWS, etc.) is near impossible without a massive marketing budget. Ad keywords are always outbid by the bigger players (Cloudflare, Google, etc). The potential is there, but it needs it&rsquo;s killer-feature moment.</li>
</ul>
<p><strong>MCPNest - Host your MCP servers in the cloud</strong></p>
<p>
<img class="img-fluid" src="./assets/image-20260111123549187.png" alt="Screenshot of MCPNest" />

</p>
<ul>
<li><strong>What:</strong> <a href="https://mcpnest.dev">https://mcpnest.dev</a> - Cloud service for running MCP servers with focus on simplicity. This was an experiment built at the peak of the MCP servers craze. It greatly simplifies re-using sets of servers, for example to have a specific set of servers + tools for a dev persona, one for a casual persona.</li>
<li><strong>How it went:</strong> 14 WAU, currently pays for itself. Financially a failure. Active dev time roughly 1 week.</li>
<li><strong>Status:</strong> Active, low maintenance. As long as it pays for itself it will continue. It’s very low maintenance and does what it claims to do very well.</li>
<li><strong>The Lesson:</strong> Even jumping on a &ldquo;hot trend&rdquo; (MCP) isn&rsquo;t enough if the audience is too niche. The timing could should have been 1-2 months faster to really hit the peak of the MCP craze.</li>
</ul>
<p><strong>Mailwire - Smart Email Proxy</strong></p>
<p>
<img class="img-fluid" src="./assets/image-20260111123706497.png" alt="Screenshot of Mailwire" />

</p>
<ul>
<li><strong>What:</strong> <a href="https://maskwire.com">Mailwire</a> is a transparent email proxy for power users. This is not a new project and already alive for a couple years, although in maintenance mode. It sits in between the users email client and their email server, and adds power-user features like auto-stripping of sensitive information or rewriting email senders (<a href="mailto:bank1@mydomain.com">bank1@mydomain.com</a>) to match the recipient address (<a href="mailto:foo@bank.com">foo@bank.com</a>). Active development time was many many months a few years ago.</li>
<li><strong>How it went</strong>: 2 active users, a big failure.</li>
<li><strong>Status:</strong> Low-energy maintenance mode. As I’m a user myself and need all of this, it will continue, but stay in maintenance and low-energy mode.</li>
<li><strong>The Lesson:</strong> This app is just too niche. I built it for myself, and turns out, I&rsquo;m the only one who really needs it.</li>
</ul>
<h3 id="group-2-the-steady-utilities">Group 2: The Steady Utilities</h3>
<p>While the ambitious platforms struggled, my small, simple utilities found a quiet, steady rhythm. These aren&rsquo;t trying to change the world, just fix a small annoyance.</p>
<p><strong>Advanced AI Actions</strong></p>
<img src="./assets/image-20260111123917852.png" alt="Screenshot of Advanced AI Actions" style="zoom:50%;" />
<ul>
<li><strong>What:</strong> One-time purchase iOS utility app to expose LLM providers through Siri Shortcuts.</li>
<li><strong>How it went:</strong> Around $15 MRR (amortized). Considering that it’s not a subscription app, I would say this is a success. I am glad people get value out of it. Active dev time was about a week, and then a few days here and there to fix bugs.</li>
<li><strong>Status:</strong> Full support going forward. Maybe migrating it to a full free-but-with-unlock app and adding a paid provider option to see if people can be converted to monthly payments.</li>
<li><strong>The Lesson:</strong> People will pay for simple utilities that solve a specific problem on their device.</li>
</ul>
<p><strong>Masked Email Manager</strong></p>
<img src="./assets/image-20260111123903570.png" alt="Screenshot of Masked Email Manager" style="zoom:50%;" />
<ul>
<li><strong>What:</strong> Utility iOS app for managing Fastmail’s Masked Email feature. Already built this before I quit my job, but I’ve been adding more features such as Siri Shortcuts support to it.</li>
<li><strong>How it went:</strong> Around 5-6 unlocks each month. I think that’s okay considering how niche it is, and being one-time purchase. Active development time of around 1 month in total.</li>
<li><strong>Status:</strong> Full support, will continue as one-time unlock utility app.</li>
<li><strong>The Lesson:</strong> Niche utilities can work if they draft off a larger platform&rsquo;s userbase (Fastmail).</li>
</ul>
<h3 id="group-3-the-consumer-bets">Group 3: The Consumer Bets</h3>
<p>This is where I saw the most actual traction—solving problems for regular people, not just other coders.</p>
<p><strong>Fix My Japanese</strong></p>
<p>
<img class="img-fluid" src="./assets/image-20260111123753384.png" alt="Screenshot of Fix My Japanese" />

</p>
<ul>
<li><strong>What:</strong> Japanese grammar and sentence structure correction through AI, created back when the bleeding edge <code>davinci</code> model was the smartest thing around. Comes as web service and iOS app that’s free with subscription.</li>
<li><strong>How it went:</strong> $14 MRR, 6 active subscriptions, 180 DAU, 700 WAU. Project is steadily growing but <strong>financially a failure</strong>. Active dev time is a couple months at this point, started way back.</li>
<li><strong>Status:</strong> Active growth. As long as it pays for itself it will stay alive. I will continue to try and integrate more paid features to see if some of those 700 WAU can be converted to paid users.</li>
<li><strong>The Lesson:</strong> Steadily growing but effectively a failure financially. It’s a great <em>free</em> product, but getting people to pay for additional functionality is hard when the free tier is &ldquo;good enough&rdquo;. Nowadays people use ChatGPT which can do 80% of what Fix My Japanese does, further making it unlikely for people to pay.</li>
</ul>
<p><strong>Quickshot Photo Editor</strong></p>
<p>
<img class="img-fluid" src="./assets/image-20260111123842155.png" alt="Screenshot of Quickshot" />

</p>
<ul>
<li><strong>What:</strong> <a href="https://quickshot.photo">Quickshot</a> is an AI-powered photo editor for iPhone (Edit photos through words, not tools). It started when photo editing with AI models became better and viable. It’s using a clever pipeline of models and AI upscaling to make targeted edits without destroying the original photo.</li>
<li><strong>How it went:</strong> 6 active subscriptions, $16 MRR. Financially a failure, but one of my apps with the most potential.</li>
<li><strong>Status:</strong> Active development. This app has potential and I will put more time into it after my current focus is completed.</li>
<li><strong>The Lesson:</strong> Despite fewer updates, this made more money than the others. It has the most potential because it appeals to a broad consumer base, but nowadays also competes against ChatGPT which is a hard uphill battle.</li>
</ul>
<h3 id="fun-stuff-the-playground">Fun stuff (The Playground)</h3>
<ul>
<li><a href="https://esim.d.sh">esimdb</a>: E-Sim comparison site using providers I frequently use and vouch for.</li>
<li><a href="https://david.coffee/projects/desks/">Desks for WeWork</a>: Native iOS app for managing WeWork bookings.</li>
<li><a href="https://theenabler.app">The Enabler</a>: Companion that always agrees with you, no matter what.</li>
</ul>
<h2 id="what-i-learned">What I learned</h2>
<p>But financials aren’t the only things I managed to learn. I learned a lot of things on the way that hopefully will help me keeping my focus going forward more sharp.</p>
<h3 id="1-always-build-with-the-monetization-strategy-in-mind">1. Always build with the monetization strategy in mind</h3>
<p>Very obvious first learning, but never build something without knowing how to monetize it. I am pretty good at keeping at that already, but some leftover apps (mainly Fix My Japanese) are fighting with the problem of converting free users to a paid tier.</p>
<h4 id="11-but-keep-a-free-tier-around-because-most-people-dont-want-to-pay-money">1.1 But, keep a free tier around because most people don’t want to pay money</h4>
<p>Getting <em>paid</em> users is tough. There’s a lot of mental steps involved to getting someone to pay for your thing, but you’ll want those users as well for growth. Offer some kind of free tier that is heavily restricted in usage and clearly pushes to the upgrade path.</p>
<h3 id="2-saas-is-hard-and-competitive">2. SaaS is hard and competitive</h3>
<p>Microfn taught me that building dev tooling and hosting services is extremely hard. The field is highly competitive with the big players (like Cloudflare workers, Firebase, Google App Engine, etc) being hard to out-maneuver for user acquisition. Can’t run ads and outbid those companies, which leaves organic growth.</p>
<h3 id="3-most-of-my-stuff-is-too-niche-focus-on-broad-consumers-instead">3. Most of my stuff is too niche, focus on broad consumers instead</h3>
<p>Yeah hard truth. Mailwire is a great transparent email proxy, but who in the world besides me needs this? Microfn is a great platform, but most people don&rsquo;t need this. MCP Nest does exactly what it ought out to do, but the audience is tiny.</p>
<p>The tech world is a tight community, <strong>better to focus on broad consumer apps instead</strong> (Quickshot, Fix My Japanese) where there&rsquo;s more room for niche players and lower customer acquisition costs.</p>
<h3 id="4-you-need-luck">4. You need luck</h3>
<p>Some apps like Quickshot that haven’t received updates still made more money than a lot of other apps that actively received new versions. Even the best idea needs the right timing and the right bit of luck to grow.</p>
<h3 id="5-indie-hacking-is-lonely">5. Indie hacking is lonely</h3>
<p>Another hard truth: indie hacking and working on stuff alone can feel lonely fast.</p>
<p>It’s a strange paradox. I have the ultimate freedom, I can travel anywhere, work from a café in Tokyo or a WeWork in Sydney, and take a random Tuesday off to go hiking. But that freedom disconnects you from the rhythm of the rest of the world.</p>
<p>My friends work full-time jobs. They can&rsquo;t do spontaneous trips. They have &ldquo;work stories&rdquo; and office drama that I can no longer relate to. There were times when I was so wired into my work, in a foreign city, that I forgot where I actually <em>was</em>.</p>
<p>Having coworkers or a community is nice, and my closest equivalent to that are currently the WeWork offices I use for working across the globe. It&rsquo;s a different kind of lonely, not sad, just&hellip; singular.</p>
<h3 id="6-marketing-is-a-skill-i-desperately-need-to-learn">6. Marketing is a skill I desperately need to learn</h3>
<p>Building a great product is only half the battle—maybe even less. I’ve realized that I have a major blind spot when it comes to marketing. Some of the services I built are technically solid and solve real problems, but I have no idea how to get them in front of the right people.</p>
<p>As a developer, it&rsquo;s easy to fall into the &ldquo;build it and they will come&rdquo; trap. But they don&rsquo;t. Marketing is a skill just like coding, and it&rsquo;s one I need to get better at if I want any of these projects to actually succeed.</p>
<h2 id="going-forward">Going forward</h2>
<p>So what is next for me in 2026?</p>
<p>I have no regrets and no plan to stop (for now), but I will focus on broader consumer apps and keep the dev-centric services in maintenance mode with minimal time put into them.</p>
<p><strong>Specific goals for 2026:</strong></p>
<ul>
<li>Reach $500 MRR by end of year</li>
<li>Focus on 2-3 core projects instead of spreading thin across 7+</li>
<li>Be more data-driven: implement proper analytics with NewRelic and Posthog dashboards to see what works and what doesn&rsquo;t</li>
<li>Double down on Quickshot and Fix My Japanese - the projects with the most traction</li>
</ul>
<p>I may need to start winding down some things in 2026 to keep my focus tight. That still feels like shooting your baby, but as my focus becomes thinner, this will become necessary.</p>
<hr>
<p>If you&rsquo;re interested in following my indie hacking journey, you can find me on <a href="https://x.com/dvcrn">Twitter/X</a> or check out my projects at <a href="https://david.coffee">david.coffee</a>. Always happy to chat with fellow indie hackers about the ups and downs of this journey.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>microfn - Your favorite toolbox for tiny, composable functions</title>
      <link>https://david.coffee/microfn/</link>
      <pubDate>Thu, 16 Oct 2025 10:00:00 +0900</pubDate>
      
      <guid>https://david.coffee/microfn/</guid><media:content url="https://david.coffee/microfn/banner.jpg" medium="image" />
          <enclosure url="https://david.coffee/microfn/banner.jpg" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/microfn/banner.jpg&#34; /&gt;&lt;/p&gt;&lt;p&gt;I want to introduce a project I&amp;rsquo;ve been working on for the past few months. I think it&amp;rsquo;s finally in a good enough state that more people can get onto it.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what microfn is about and all the cool stuff you can do with it!&lt;/p&gt;
&lt;h2 id=&#34;tiny-composable-functions&#34;&gt;Tiny composable functions&lt;/h2&gt;
&lt;p&gt;Microfn is all about small functions (hence the name &lt;em&gt;micro&lt;/em&gt;) - you have a small piece of functionality in the form of a function and want to run it somewhere. Well microfn is just the place for that! We run that function for you and make sure it is available for whenever you need it.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s say I have this snippet here that just queries wttr.in to get the current weather:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;url&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;https://wttr.in/Tokyo?format=3&amp;amp;m&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;response&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;url&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;response&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#66d9ef&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Error(&lt;span style=&#34;color:#e6db74&#34;&gt;`HTTP error! status: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;response&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;status&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;response&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;text&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  } &lt;span style=&#34;color:#66d9ef&#34;&gt;catch&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;error&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;console&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;error&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Error fetching weather:&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;error&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;error&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Failed to fetch weather data&amp;#34;&lt;/span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We can quickly deploy this to microfn in seconds by writing the code, hitting save and it&amp;rsquo;s deployed. Now that it&amp;rsquo;s deployed we can do all kinds of stuff with it!&lt;/p&gt;
&lt;h2 id=&#34;functions-from-anywhere&#34;&gt;Functions from anywhere&lt;/h2&gt;
&lt;p&gt;By default, microfn gives you a webhook that you can call in the form of&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;curl -X POST https://microfn.dev/run/david/get-tokyo-weather
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Executing this curl will, as you may have guessed, get the current Tokyo weather by executing your function on the microfn platform.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./curl.png&#34; alt=&#34;Curl execution&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;But we can do more!&lt;/p&gt;
&lt;h2 id=&#34;functions-meet-other-functions&#34;&gt;Functions meet other functions&lt;/h2&gt;
&lt;p&gt;Isolated functions are already useful on their own, but they become even more powerful when we allow functions to interact with each other.&lt;/p&gt;
&lt;p&gt;For example, we can encapsulate recurring logic to send a discord message into a &lt;a href=&#34;https://microfn.dev/david/send-discord-message/&#34;&gt;send-discord-message&lt;/a&gt; function:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;input&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;channelId&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;string&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;content&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;string&lt;/span&gt; }) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;secret&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;getRequired&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;DISCORD_BOT_TOKEN&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;channelId&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;content&lt;/span&gt; } &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;input&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;discordPayload&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;channelId&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;content&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  };
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;response&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;`https://discord.com/api/v10/channels/&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;discordPayload&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;channelId&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;/messages`&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;method&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;POST&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;headers&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;Authorization&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`Bot &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;body&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;JSON&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;stringify&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;content&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;discordPayload&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;content&lt;/span&gt; }),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;response&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;json&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;response&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Error(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#e6db74&#34;&gt;`Discord API error: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;response&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;status&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;JSON&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;stringify&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;)&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We could already plug the webhook into places where we want to send a discord message, effectively giving us a curl-to-discord bridge that hides away secrets and logic: Calling the curl will send a message.&lt;/p&gt;
&lt;p&gt;But we can also hook this up to other functions that want to send discord messages:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;@microfn/fn&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;weather&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fn&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;executeFunction&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;david/get-tokyo-weather&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fn&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;executeFunction&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;david/send-discord-message&amp;#34;&lt;/span&gt;, {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;channelId&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;1419082455608197140&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;content&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;weather&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;See what we did there? We hooked our 2 functions up into a new function that composes them and sends the current weather into discord!&lt;/p&gt;
&lt;h2 id=&#34;automation-triggers&#34;&gt;Automation triggers&lt;/h2&gt;
&lt;p&gt;Beyond manually calling functions, microfn supports automatic triggers that execute your functions based on events.&lt;/p&gt;
&lt;h3 id=&#34;scheduled-execution&#34;&gt;Scheduled execution&lt;/h3&gt;
&lt;p&gt;Functions can be executed on a schedule through CRON:&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./cron-schedule.jpg&#34; alt=&#34;The CRON trigger&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;With just 2 clicks, we turned our function into an hourly weather notifier in discord!&lt;/p&gt;
&lt;h3 id=&#34;email-triggers&#34;&gt;Email triggers&lt;/h3&gt;
&lt;p&gt;We can also set up functions to respond to emails. Enabling the email trigger gives you a new email address in the form of &lt;code&gt;fn-cdmixwn7kw7p@mail.microfn.dev&lt;/code&gt;. Any email sent to this address will automatically trigger your function:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;@microfn/fn&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;email&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;email&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Email&lt;/span&gt; }) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;email&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;body&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;indexOf&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;microfn&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;content&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`New email from &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;email&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;from&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;email&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;subject&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fn&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;executeFunction&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;david/send-discord-message&amp;#34;&lt;/span&gt;, {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;channelId&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;1419082455608197140&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;content&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;content&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now we have a discord notifier for whenever someone sends us an email with the word &amp;ldquo;microfn&amp;rdquo; in the body.&lt;/p&gt;
&lt;p&gt;This makes it easy to handle things like Google Voice SMS-to-email forwarding - just parse the email body in the &lt;code&gt;email()&lt;/code&gt; callback, extract the SMS content, and forward it wherever you need!&lt;/p&gt;
&lt;h2 id=&#34;functions-meet-ai&#34;&gt;Functions meet AI&lt;/h2&gt;
&lt;p&gt;Now here&amp;rsquo;s where things get really interesting. Tiny functions are a fantastic candidate to be used as AI tools - they encapsulate logic, do one job, and are easy to reason with.&lt;/p&gt;
&lt;p&gt;We can hook microfn up to an LLM through an MCP and ask it to get the Tokyo weather:&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./ai-execution.jpg&#34; alt=&#34;Claude meets microfn&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;But what&amp;rsquo;s even cooler is that we can ask AI to make changes to our functions to give itself new skills. One simple prompt, and the AI does the rest!&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./ai-edit.jpg&#34; alt=&#34;Claude uses microfn&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;And executing this new variant gives us this result:&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./ai-execution-2.jpg&#34; alt=&#34;Claude uses new function&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;This turns microfn into a self-expanding toolbox for your AI assistant. Something missing? Just have AI write its own skill! No need to install new MCP servers just to add one extra capability.&lt;/p&gt;
&lt;h2 id=&#34;real-world-integrations&#34;&gt;Real-world integrations&lt;/h2&gt;
&lt;p&gt;Your functions are like micro tools that you can plug into anything: MCP, Siri Shortcuts, GitHub Actions, Webhooks, Emails, Cron - you name it!&lt;/p&gt;
&lt;p&gt;Here are some concrete examples of what you can build:&lt;/p&gt;
&lt;h3 id=&#34;github-webhook-handler&#34;&gt;GitHub webhook handler&lt;/h3&gt;
&lt;p&gt;Create a &lt;code&gt;github-webhook&lt;/code&gt; function and hook it up as your GitHub webhook URL. Now you have an entry funnel for GitHub events.&lt;/p&gt;
&lt;p&gt;Compose it with your discord or slack notification function, and you get real-time notifications with custom parsing for whenever a new issue gets opened or a PR is created:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;@microfn/fn&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;input&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;action&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;string&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;issue&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;any&lt;/span&gt; }) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;input&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;action&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;===&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;opened&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;message&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`New issue: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;input&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;issue&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;title&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;input&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;issue&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;html_url&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fn&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;executeFunction&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;david/send-discord-message&amp;#34;&lt;/span&gt;, {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;channelId&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;1419082455608197140&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;content&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;message&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;twilio-sms-automation&#34;&gt;Twilio SMS automation&lt;/h3&gt;
&lt;p&gt;Hook a function up as a Twilio webhook when a new message has been received and you have programmable SMS funneling in seconds:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;@microfn/fn&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;input&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;From&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;string&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;Body&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;string&lt;/span&gt; }) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// Forward SMS to Telegram, Discord, or anywhere else
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;message&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`SMS from &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;input&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;From&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;input&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Body&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fn&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;executeFunction&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;david/send-discord-message&amp;#34;&lt;/span&gt;, {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;channelId&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;1419082455608197140&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;content&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;message&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now all your SMS messages are in one place, with all the filtering and routing logic you need.&lt;/p&gt;
&lt;h3 id=&#34;release-notifications&#34;&gt;Release notifications&lt;/h3&gt;
&lt;p&gt;Want release notifications for your projects? Create a &lt;code&gt;release-notifier&lt;/code&gt; function, add the webhook to your GitHub release workflow or deployment script, and you have programmable release notifications:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;@microfn/fn&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;input&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;version&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;string&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;notes&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;string&lt;/span&gt; }) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;message&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`🚀 New release: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;input&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;version&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;${&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;input&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;notes&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// Send to multiple channels at once
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fn&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;executeFunction&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;david/send-discord-message&amp;#34;&lt;/span&gt;, {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;channelId&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;1419082455608197140&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;content&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;message&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// Also post to Twitter, send to Slack, update a status page, etc.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;// All in one function!
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;your-toolbox-ready-for-when-you-need-it&#34;&gt;Your toolbox, ready for when you need it!&lt;/h2&gt;
&lt;p&gt;Microfn gets your functions running &lt;em&gt;quick&lt;/em&gt;! Think your local &lt;code&gt;npm&lt;/code&gt;, but in the cloud - you have your function, you hit save, and it&amp;rsquo;s deployed. Done.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s your toolbox for tiny stuff, your hammers, drivers and drills, ready instantly for when you need it.&lt;/p&gt;
&lt;p&gt;I am using microfn heavily myself and in the short time of using it, it has already positioned itself as an invaluable tool and in the middle of most of my automation flows.&lt;/p&gt;
&lt;p&gt;Microfn comes with a free tier to try things out, and a flat $8/mo for the full version.&lt;/p&gt;
&lt;p&gt;Give it a try and let us know what you think!&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/microfn/banner.jpg" /></p><p>I want to introduce a project I&rsquo;ve been working on for the past few months. I think it&rsquo;s finally in a good enough state that more people can get onto it.</p>
<p>Here&rsquo;s what microfn is about and all the cool stuff you can do with it!</p>
<h2 id="tiny-composable-functions">Tiny composable functions</h2>
<p>Microfn is all about small functions (hence the name <em>micro</em>) - you have a small piece of functionality in the form of a function and want to run it somewhere. Well microfn is just the place for that! We run that function for you and make sure it is available for whenever you need it.</p>
<p>Let&rsquo;s say I have this snippet here that just queries wttr.in to get the current weather:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">url</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://wttr.in/Tokyo?format=3&amp;m&#34;</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">response</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fetch</span>(<span style="color:#a6e22e">url</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">ok</span>) {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> Error(<span style="color:#e6db74">`HTTP error! status: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">status</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">text</span>();
</span></span><span style="display:flex;"><span>  } <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">error</span>(<span style="color:#e6db74">&#34;Error fetching weather:&#34;</span>, <span style="color:#a6e22e">error</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> { <span style="color:#a6e22e">error</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;Failed to fetch weather data&#34;</span> };
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>We can quickly deploy this to microfn in seconds by writing the code, hitting save and it&rsquo;s deployed. Now that it&rsquo;s deployed we can do all kinds of stuff with it!</p>
<h2 id="functions-from-anywhere">Functions from anywhere</h2>
<p>By default, microfn gives you a webhook that you can call in the form of</p>
<pre tabindex="0"><code>curl -X POST https://microfn.dev/run/david/get-tokyo-weather
</code></pre><p>Executing this curl will, as you may have guessed, get the current Tokyo weather by executing your function on the microfn platform.</p>
<p>
<img class="img-fluid" src="./curl.png" alt="Curl execution" />

</p>
<p>But we can do more!</p>
<h2 id="functions-meet-other-functions">Functions meet other functions</h2>
<p>Isolated functions are already useful on their own, but they become even more powerful when we allow functions to interact with each other.</p>
<p>For example, we can encapsulate recurring logic to send a discord message into a <a href="https://microfn.dev/david/send-discord-message/">send-discord-message</a> function:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">main</span>(<span style="color:#a6e22e">input</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">channelId</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>, <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span> }) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">token</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">secret</span>.<span style="color:#a6e22e">getRequired</span>(<span style="color:#e6db74">&#34;DISCORD_BOT_TOKEN&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> { <span style="color:#a6e22e">channelId</span>, <span style="color:#a6e22e">content</span> } <span style="color:#f92672">=</span> <span style="color:#a6e22e">input</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">discordPayload</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">channelId</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">content</span>
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">response</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fetch</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">`https://discord.com/api/v10/channels/</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">discordPayload</span>.<span style="color:#a6e22e">channelId</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/messages`</span>,
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">method</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;POST&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Authorization</span><span style="color:#f92672">:</span> <span style="color:#e6db74">`Bot </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">token</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Content-Type&#34;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;application/json&#34;</span>,
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">body</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">stringify</span>({ <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">discordPayload</span>.<span style="color:#a6e22e">content</span> }),
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">data</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">json</span>();
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">ok</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> Error(
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">`Discord API error: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">status</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">stringify</span>(<span style="color:#a6e22e">data</span>)<span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>,
</span></span><span style="display:flex;"><span>    );
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">data</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>We could already plug the webhook into places where we want to send a discord message, effectively giving us a curl-to-discord bridge that hides away secrets and logic: Calling the curl will send a message.</p>
<p>But we can also hook this up to other functions that want to send discord messages:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">fn</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;@microfn/fn&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">weather</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fn</span>.<span style="color:#a6e22e">executeFunction</span>(<span style="color:#e6db74">&#34;david/get-tokyo-weather&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fn</span>.<span style="color:#a6e22e">executeFunction</span>(<span style="color:#e6db74">&#34;david/send-discord-message&#34;</span>, {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">channelId</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;1419082455608197140&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">weather</span>,
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>See what we did there? We hooked our 2 functions up into a new function that composes them and sends the current weather into discord!</p>
<h2 id="automation-triggers">Automation triggers</h2>
<p>Beyond manually calling functions, microfn supports automatic triggers that execute your functions based on events.</p>
<h3 id="scheduled-execution">Scheduled execution</h3>
<p>Functions can be executed on a schedule through CRON:</p>
<p>
<img class="img-fluid" src="./cron-schedule.jpg" alt="The CRON trigger" />

</p>
<p>With just 2 clicks, we turned our function into an hourly weather notifier in discord!</p>
<h3 id="email-triggers">Email triggers</h3>
<p>We can also set up functions to respond to emails. Enabling the email trigger gives you a new email address in the form of <code>fn-cdmixwn7kw7p@mail.microfn.dev</code>. Any email sent to this address will automatically trigger your function:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">fn</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;@microfn/fn&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">email</span>(<span style="color:#a6e22e">data</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">email</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">Email</span> }) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">email</span>.<span style="color:#a6e22e">body</span>.<span style="color:#a6e22e">indexOf</span>(<span style="color:#e6db74">&#34;microfn&#34;</span>) <span style="color:#f92672">!=</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">content</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`New email from </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">email</span>.<span style="color:#a6e22e">from</span><span style="color:#e6db74">}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">email</span>.<span style="color:#a6e22e">subject</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fn</span>.<span style="color:#a6e22e">executeFunction</span>(<span style="color:#e6db74">&#34;david/send-discord-message&#34;</span>, {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">channelId</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;1419082455608197140&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">content</span>,
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now we have a discord notifier for whenever someone sends us an email with the word &ldquo;microfn&rdquo; in the body.</p>
<p>This makes it easy to handle things like Google Voice SMS-to-email forwarding - just parse the email body in the <code>email()</code> callback, extract the SMS content, and forward it wherever you need!</p>
<h2 id="functions-meet-ai">Functions meet AI</h2>
<p>Now here&rsquo;s where things get really interesting. Tiny functions are a fantastic candidate to be used as AI tools - they encapsulate logic, do one job, and are easy to reason with.</p>
<p>We can hook microfn up to an LLM through an MCP and ask it to get the Tokyo weather:</p>
<p>
<img class="img-fluid" src="./ai-execution.jpg" alt="Claude meets microfn" />

</p>
<p>But what&rsquo;s even cooler is that we can ask AI to make changes to our functions to give itself new skills. One simple prompt, and the AI does the rest!</p>
<p>
<img class="img-fluid" src="./ai-edit.jpg" alt="Claude uses microfn" />

</p>
<p>And executing this new variant gives us this result:</p>
<p>
<img class="img-fluid" src="./ai-execution-2.jpg" alt="Claude uses new function" />

</p>
<p>This turns microfn into a self-expanding toolbox for your AI assistant. Something missing? Just have AI write its own skill! No need to install new MCP servers just to add one extra capability.</p>
<h2 id="real-world-integrations">Real-world integrations</h2>
<p>Your functions are like micro tools that you can plug into anything: MCP, Siri Shortcuts, GitHub Actions, Webhooks, Emails, Cron - you name it!</p>
<p>Here are some concrete examples of what you can build:</p>
<h3 id="github-webhook-handler">GitHub webhook handler</h3>
<p>Create a <code>github-webhook</code> function and hook it up as your GitHub webhook URL. Now you have an entry funnel for GitHub events.</p>
<p>Compose it with your discord or slack notification function, and you get real-time notifications with custom parsing for whenever a new issue gets opened or a PR is created:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">fn</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;@microfn/fn&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">main</span>(<span style="color:#a6e22e">input</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">action</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>, <span style="color:#a6e22e">issue</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">any</span> }) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">input</span>.<span style="color:#a6e22e">action</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#34;opened&#34;</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">message</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`New issue: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">input</span>.<span style="color:#a6e22e">issue</span>.<span style="color:#a6e22e">title</span><span style="color:#e6db74">}</span><span style="color:#e6db74">\n</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">input</span>.<span style="color:#a6e22e">issue</span>.<span style="color:#a6e22e">html_url</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fn</span>.<span style="color:#a6e22e">executeFunction</span>(<span style="color:#e6db74">&#34;david/send-discord-message&#34;</span>, {
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">channelId</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;1419082455608197140&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">message</span>,
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="twilio-sms-automation">Twilio SMS automation</h3>
<p>Hook a function up as a Twilio webhook when a new message has been received and you have programmable SMS funneling in seconds:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">fn</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;@microfn/fn&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">main</span>(<span style="color:#a6e22e">input</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">From</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>, <span style="color:#a6e22e">Body</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span> }) {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// Forward SMS to Telegram, Discord, or anywhere else
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">message</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`SMS from </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">input</span>.<span style="color:#a6e22e">From</span><span style="color:#e6db74">}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">input</span>.<span style="color:#a6e22e">Body</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fn</span>.<span style="color:#a6e22e">executeFunction</span>(<span style="color:#e6db74">&#34;david/send-discord-message&#34;</span>, {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">channelId</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;1419082455608197140&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">message</span>,
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now all your SMS messages are in one place, with all the filtering and routing logic you need.</p>
<h3 id="release-notifications">Release notifications</h3>
<p>Want release notifications for your projects? Create a <code>release-notifier</code> function, add the webhook to your GitHub release workflow or deployment script, and you have programmable release notifications:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">fn</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;@microfn/fn&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">main</span>(<span style="color:#a6e22e">input</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">version</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>, <span style="color:#a6e22e">notes</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span> }) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">message</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`🚀 New release: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">input</span>.<span style="color:#a6e22e">version</span><span style="color:#e6db74">}</span><span style="color:#e6db74">\n</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">input</span>.<span style="color:#a6e22e">notes</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// Send to multiple channels at once
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fn</span>.<span style="color:#a6e22e">executeFunction</span>(<span style="color:#e6db74">&#34;david/send-discord-message&#34;</span>, {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">channelId</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;1419082455608197140&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">content</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">message</span>,
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// Also post to Twitter, send to Slack, update a status page, etc.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">// All in one function!
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><h2 id="your-toolbox-ready-for-when-you-need-it">Your toolbox, ready for when you need it!</h2>
<p>Microfn gets your functions running <em>quick</em>! Think your local <code>npm</code>, but in the cloud - you have your function, you hit save, and it&rsquo;s deployed. Done.</p>
<p>It&rsquo;s your toolbox for tiny stuff, your hammers, drivers and drills, ready instantly for when you need it.</p>
<p>I am using microfn heavily myself and in the short time of using it, it has already positioned itself as an invaluable tool and in the middle of most of my automation flows.</p>
<p>Microfn comes with a free tier to try things out, and a flat $8/mo for the full version.</p>
<p>Give it a try and let us know what you think!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>I finally understand Cloudflare Zero Trust tunnels</title>
      <link>https://david.coffee/cloudflare-zero-trust-tunnels/</link>
      <pubDate>Mon, 22 Sep 2025 10:00:00 +0900</pubDate>
      
      <guid>https://david.coffee/cloudflare-zero-trust-tunnels/</guid><media:content url="https://david.coffee/cloudflare-zero-trust-tunnels/banner.jpeg" medium="image" />
          <enclosure url="https://david.coffee/cloudflare-zero-trust-tunnels/banner.jpeg" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/cloudflare-zero-trust-tunnels/banner.jpeg&#34; /&gt;&lt;/p&gt;&lt;p&gt;A while ago, after frustration with Tailscale in environments where it couldn&amp;rsquo;t properly penetrate NAT/firewall and get a p2p connection, I decided to invest some time into learning something new: Cloudflare Zero Trust + Warp.&lt;/p&gt;
&lt;p&gt;There are so many new concepts, but after way too long, I can finally say that &lt;strong&gt;I understand Cloudflare Zero Trust Warp now&lt;/strong&gt;. I am a full-on Cloudflare Zero Trust with Warp convert, and while I still have Tailscale running in parallel, almost everything I do now is going through Zero Trust tunnels.&lt;/p&gt;
&lt;p&gt;This post is an explanation of the basic concepts, because I’m sure others will have similar issues wrapping their head around it.&lt;/p&gt;
&lt;h4 id=&#34;why-tho&#34;&gt;Why tho?&lt;/h4&gt;
&lt;p&gt;Why would you even sink so much time into learning this? What does it give you?&lt;/p&gt;
&lt;p&gt;Argo tunnels through Zero Trust allow you to do a bunch of &lt;em&gt;really&lt;/em&gt; cool things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Connect private networks together - can be home networks, can be kubernetes clusters, you can create tunnels to and from every infra&lt;/li&gt;
&lt;li&gt;Expose private services to the public, on public hostnames, no matter where they are running. You could even put your router running at 192.168.1.1 on the internet, accessible to everyone, no Warp client required&lt;/li&gt;
&lt;li&gt;Create fully private networks with private IPs (10.x.x.x) that only resolve when Warp is connected, to services you specify&lt;/li&gt;
&lt;li&gt;Quickly expose a public route to any service running locally or on any server, for quick development, testing webhooks or giving coworkers a quick preview&lt;/li&gt;
&lt;li&gt;Create a fully private network running at home that&amp;rsquo;s only available when you&amp;rsquo;re connected to the Warp VPN client, or only to you, reachable anywhere&lt;/li&gt;
&lt;li&gt;No worries about NAT, everything goes through the Cloudflare network, no direct p2p connection required&lt;/li&gt;
&lt;li&gt;Add very granular access policies on who can access what - what login method does the user need, which email addresses are allowed. Allow bots and server-to-server exceptions with service access tokens.
&lt;ul&gt;
&lt;li&gt;Does the user need to have Warp running? Does he need to be enrolled in Zero Trust? Does he need some special permission flag?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Authenticate to SSH servers through Zero Trust access policies without the need of SSH keys. Just connect Warp, type &lt;code&gt;ssh host&lt;/code&gt; and you&amp;rsquo;re logged in
&lt;ul&gt;
&lt;li&gt;Close public SSH ports completely to only allow login through Warp&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Get the benefits of Cloudflare VPN edge routing on top (similar to 1.1.1.1 Warp+)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;quickie-cloudflare-zero-trust-vs-tailscale&#34;&gt;Quickie: Cloudflare Zero Trust vs Tailscale&lt;/h2&gt;
&lt;p&gt;To get this out of the way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tailscale&lt;/strong&gt;: peer-to-peer, uses NAT and firewall penetration methods to establish p2p connections. If not possible, it goes through central relay servers. Absolute best speed and latency if a connection is established.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare&lt;/strong&gt;: All traffic (with the exception of warp-to-warp routing, which is p2p) goes through Cloudflare&amp;rsquo;s edge network. So even SSH-ing into your local router will hop through Cloudflare servers. This adds latency, but no issues with NAT at all.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;cloudflared--warp&#34;&gt;Cloudflared != Warp&lt;/h2&gt;
&lt;p&gt;Cloudflare has 2 tools available: &lt;a href=&#34;https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/&#34;&gt;Warp Client&lt;/a&gt; and &lt;a href=&#34;https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/&#34;&gt;Cloudflared&lt;/a&gt;. They interact with each other and have similarities in some areas but are not the same.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Warp Client&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The tool that connects you to the Cloudflare network. This is the thing that you configure to add clients into your Zero Trust network and enforces policies.&lt;/p&gt;
&lt;p&gt;Usually this runs on clients, but can also run on servers.&lt;/p&gt;
&lt;p&gt;Warp client also supports &lt;a href=&#34;https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/private-net/warp-to-warp/&#34;&gt;warp-to-warp routing&lt;/a&gt; which is a true p2p connection similar to Tailscale.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cloudflared&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The thing that creates a &lt;strong&gt;tunnel&lt;/strong&gt; and adds it to the Zero Trust network.&lt;/p&gt;
&lt;p&gt;Most commonly you run this on servers to expose tunnels into your network, but you can also run it on clients.&lt;/p&gt;
&lt;p&gt;On the client side you can use &lt;code&gt;cloudflared access&lt;/code&gt; to establish a connection with other things in your Zero Trust network.&lt;/p&gt;
&lt;p&gt;Can also create one-time-use tunnels that aren’t connected to the Zero Trust network. Good for testing.&lt;/p&gt;
&lt;h2 id=&#34;tunnels-routes-targets&#34;&gt;Tunnels, Routes, Targets&lt;/h2&gt;
&lt;p&gt;This took me the longest to understand. Zero Trust allows you to configure &lt;em&gt;Tunnels&lt;/em&gt;, &lt;em&gt;Routes&lt;/em&gt; and &lt;em&gt;Targets&lt;/em&gt;; here’s how they interplay.&lt;/p&gt;
&lt;h3 id=&#34;tunnels&#34;&gt;Tunnels&lt;/h3&gt;
&lt;p&gt;The most important part of your setup. Tunnels are deployed through &lt;code&gt;cloudflared&lt;/code&gt; and are simply an &lt;em&gt;exit&lt;/em&gt; for traffic. Think of it as a literal tunnel that has its end somewhere.&lt;/p&gt;
&lt;p&gt;Tunnels are deployed to infrastructure in the target network. So if you have a home network with 192.168.1.1/24, you want to deploy &lt;code&gt;cloudflared&lt;/code&gt; on any machine that’s always on and within that network. It can be your router, or your Raspi, it doesn&amp;rsquo;t matter.&lt;/p&gt;
&lt;p&gt;For server-hosted services, you can have a tunnel on your main dev server, on a server, or on a pod in your Kubernetes cluster.&lt;/p&gt;
&lt;p&gt;Now you have an opening into these networks through Warp/Argo tunnels.&lt;/p&gt;
&lt;h4 id=&#34;configuring-tunnels&#34;&gt;Configuring tunnels&lt;/h4&gt;
&lt;p&gt;You can either &lt;a href=&#34;https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/local-management/configuration-file/&#34;&gt;configure&lt;/a&gt; tunnels through the Zero Trust UI by “adopting” them, or configure them in the &lt;code&gt;/etc/cloudflared/config.yml&lt;/code&gt; config on the machine itself. Personal preference, I usually configure them on the machine itself.&lt;/p&gt;
&lt;p&gt;The config specifies where a request should get routed to when it arrives at the tunnel. So the tunnel knows what to do with it.&lt;/p&gt;
&lt;p&gt;In this config we tell cloudflared to route traffic arriving at this tunnel for hostname &lt;code&gt;gitlab.widgetcorp.tech&lt;/code&gt; to localhost:80, and &lt;code&gt;gitlab-ssh&lt;/code&gt; to the local SSH server.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;❯ cat /etc/cloudflared/config.yml
tunnel: a2f17e27-cd4d-4fcd-b02a-63839f57a96f
credentials-file: /etc/cloudflared/a2f17e27-cd4d-4fcd-b02a-63839f57a96f.json
ingress:
  - hostname: gitlab.widgetcorp.tech
    service: http://localhost:80
  - hostname: gitlab-ssh.widgetcorp.tech
    service: ssh://localhost:22
  - service: http_status:404

  # Catch-all for WARP routing
  - service: http_status:404

warp-routing:
  enabled: true
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The config alone doesn’t do anything. It just exposes a tunnel, and that&amp;rsquo;s it. What we need now are routes and targets.&lt;/p&gt;
&lt;h4 id=&#34;exposing-a-private-network-to-the-public-with-tunnels-quickly&#34;&gt;Exposing a private network to the public with tunnels quickly&lt;/h4&gt;
&lt;p&gt;Quick addition, as this is a super common use case. If you want to just expose something in your home network to the internet, you can add a config like this:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;tunnel: a2f17e27-cd4d-4fcd-b02a-63839f57a96f
credentials-file: /etc/cloudflared/a2f17e27-cd4d-4fcd-b02a-63839f57a96f.json
ingress:
  - hostname: homeassistant.mydomain.com
    service: http://192.168.1.3:80
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then go into Cloudflare DNS settings and map the domain &lt;code&gt;homeassistant.mydomain.com&lt;/code&gt; to the tunnel:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CNAME homeassistant.mydomain.com a2f17e27-cd4d-4fcd-b02a-63839f57a96f.cfargotunnel.com&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Now all traffic going to this domain will go through the cloudflared tunnel, which is configured to route &lt;code&gt;homeassistant.mydomain.com&lt;/code&gt; to &lt;code&gt;192.168.1.3&lt;/code&gt;. &lt;strong&gt;No Warp client needed&lt;/strong&gt;, Argo tunnel does everything for us.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you adopted the tunnels and don’t use &lt;code&gt;config.yaml&lt;/code&gt;, you can automatically create matching DNS records in the Cloudflare UI and don’t need to do this manually.&lt;/p&gt;
&lt;h3 id=&#34;routes&#34;&gt;Routes&lt;/h3&gt;
&lt;p&gt;A route defines &lt;em&gt;where&lt;/em&gt; to direct traffic to.&lt;/p&gt;
&lt;p&gt;Let’s say your homeassistant runs on 192.168.1.3 at home and you want to reach it from outside. Just above we deployed a &lt;code&gt;cloudflared&lt;/code&gt; tunnel on our router at 192.168.1.3, and added a config pointing the domain to the Argo tunnel, so &lt;code&gt;homeassistant.mydomain.com&lt;/code&gt; is already available to the public. However, &lt;code&gt;192.168.1.3&lt;/code&gt; isn’t, as it’s a private network IP.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;routes-configure.png&#34; alt=&#34;Configuring a route in Zero Trust&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;You can define:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A route like &lt;code&gt;192.168.1.1/24&lt;/code&gt; pointing at your tunnel, to route &lt;em&gt;ALL&lt;/em&gt; traffic to the full IP range through that tunnel (so even 192.168.1.245 will go through your tunnel)&lt;/li&gt;
&lt;li&gt;Or a more specific route like &lt;code&gt;192.168.1.3/32&lt;/code&gt; pointing at your tunnel, to &lt;em&gt;ONLY&lt;/em&gt; route traffic to 192.168.1.3 through that tunnel.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When configured, once your user connects their Warp client that&amp;rsquo;s set up with your Zero Trust network, the Warp client will see requests to 192.168.1.3 and route it through the Cloudflare network to reach your specific tunnel. Like a little police helper directing cars where to go.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;If the Warp client is &lt;strong&gt;not&lt;/strong&gt; connected, 192.168.1.3 will just resolve in your current local network. If connected, it will resolve to the tunnel.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The routed IP doesn’t need to exist!&lt;/strong&gt; So you could, for example, route a random IP you like (e.g., 10.128.1.1) to your tunnel, the tunnel then forwards it based on your routes, for example to 192.168.1.1. This is extremely powerful because it allows you to build your own fully virtual network.&lt;/p&gt;
&lt;p&gt;That’s all it does, what happens afterwards is up to the tunnel config that we created above. The tunnel decides where to point the incoming request to, whether that’s localhost or somewhere else.&lt;/p&gt;
&lt;p&gt;To summarize, the &lt;code&gt;route&lt;/code&gt; tells the Warp client where to route traffic to.&lt;/p&gt;
&lt;p&gt;Now we have 2 things working:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;homeassistant.mydomain.com&lt;/code&gt; - goes through a Cloudflare DNS record pointing at an Argo tunnel, which then forwards to 192.168.1.3. This works without Warp connected as it&amp;rsquo;s on the DNS level, public to everyone.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;192.168.1.3&lt;/code&gt; - The Warp client sees the request and routes it through the Argo tunnel, which then forwards it to &lt;code&gt;192.168.1.3&lt;/code&gt; within that network. This needs Warp connected to work, and is only visible to people in your Zero Trust org.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;targets&#34;&gt;Targets&lt;/h3&gt;
&lt;p&gt;This one took me a while.&lt;/p&gt;
&lt;p&gt;Targets are needed to &lt;em&gt;define a piece of infrastructure&lt;/em&gt; that you want to protect through Zero Trust. They are like a pointer pointing to something in your network. This goes hand-in-hand with routes, but isn’t always needed.&lt;/p&gt;
&lt;p&gt;Let’s say you have 192.168.1.3 (homeassistant) exposed through a Cloudflare tunnel. By default, anyone in your network that is part of your Zero Trust org and has Warp client installed can now access your homeassistant at 192.168.1.3.&lt;/p&gt;
&lt;p&gt;We can change that with targets. For example, defining a target with hostname = &lt;code&gt;homeassistant.mydomain.com&lt;/code&gt; to the route &lt;code&gt;192.168.1.3/32&lt;/code&gt; allows us to add access policies to it. We can also put an entire network into the target by specifying &lt;code&gt;192.168.1.3/24&lt;/code&gt; to control access. This also works with virtual IPs like 10.128.1.1!&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./targets-config-screen.png&#34; alt=&#34;Configuring a target in Zero Trust&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Targets alone won’t do anything, they just point to the service or network. “Hey, here is homeassistant”, or “hey, here is my home network”.&lt;/p&gt;
&lt;h2 id=&#34;access-policies-protecting-who-can-access-what&#34;&gt;Access Policies: Protecting Who Can Access What&lt;/h2&gt;
&lt;p&gt;Continuing the example from above:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;we have a tunnel running on our home network that routes &lt;code&gt;homeassistant.mydomain.com&lt;/code&gt; to &lt;code&gt;192.168.1.3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;we set up public DNS records to point &lt;code&gt;homeassistant.mydomain.com&lt;/code&gt; to the Argo tunnel in Cloudflare&lt;/li&gt;
&lt;li&gt;we created a &lt;em&gt;route&lt;/em&gt; &lt;code&gt;192.168.1.3&lt;/code&gt; to go through the same tunnel&lt;/li&gt;
&lt;li&gt;we also created a &lt;em&gt;target&lt;/em&gt; pointing to &lt;code&gt;192.168.1.3&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When users access either &lt;code&gt;192.168.1.3&lt;/code&gt; or &lt;code&gt;homeassistant.mydomain.com&lt;/code&gt;, the Warp client will route the request through the tunnel, which then forwards the request to 192.168.1.3. Homeassistant loads and everything is fine.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;But do we want that?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Probably not.&lt;/p&gt;
&lt;p&gt;Access policies to the rescue!&lt;/p&gt;
&lt;p&gt;With access policies, we can leave things in the public but protect them with Cloudflare Zero Trust access. So while 192.168.1.3 is only available if Warp is connected (so routing to it works), we can add security to our public &lt;code&gt;homeassistant.mydomain.com&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Go to Access -&amp;gt; Applications -&amp;gt; Add an Application -&amp;gt; Self-hosted.&lt;/p&gt;
&lt;p&gt;Here we can define &lt;em&gt;what&lt;/em&gt; should be protected, and &lt;em&gt;how&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Going with our previous example, we can add a public hostname &lt;code&gt;homeassistant.mydomain.com&lt;/code&gt; or an IP like &lt;code&gt;192.168.1.3&lt;/code&gt; (or both), then attach policies of who should be able to access it.&lt;/p&gt;
&lt;p&gt;You can specify &lt;strong&gt;Include&lt;/strong&gt; (“OR”) and &lt;strong&gt;Require&lt;/strong&gt; (“AND”) selectors.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Require&lt;/strong&gt; rules must always be met, &lt;em&gt;on top&lt;/em&gt; of include rules, to grant access&lt;/li&gt;
&lt;li&gt;Any of the &lt;strong&gt;Include&lt;/strong&gt; rules must match to grant access&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then there are &lt;em&gt;Actions&lt;/em&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Allow&lt;/strong&gt; - when the policy matches, allow access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deny&lt;/strong&gt; - when the policy matches, deny access. aka blocking something.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bypass&lt;/strong&gt; - when the policy matches, bypass Zero Trust completely. No more checking.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service Auth&lt;/strong&gt; - when the policy matches, allow authentication to the service with a service token header (good for server-to-server, or bots). Check Access -&amp;gt; Service Auth to create these tokens.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;allow-public-access-to-everyone-logging-into-your-network&#34;&gt;Allow public access to everyone logging into your network&lt;/h3&gt;
&lt;p&gt;The most common use case: &lt;code&gt;homeassistant.mydomain.com&lt;/code&gt; is public. We want to keep it public, but add an extra layer of security.&lt;/p&gt;
&lt;p&gt;Add an &lt;em&gt;include&lt;/em&gt; policy, pick any of the &lt;code&gt;email&lt;/code&gt; selectors, add the email of the user you want to allow access to. Now only people authenticated with your Zero Trust org with the specified emails can access your homeassistant, without needing to have Warp running.&lt;/p&gt;
&lt;p&gt;We can harden this by adding &lt;em&gt;require&lt;/em&gt; rules: Add a &lt;em&gt;Login Method&lt;/em&gt; selector rule, pick a specific login method like GitHub. Now only people with specific emails that have authenticated through GitHub can access your homeassistant, without needing to have Warp running.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./policy-github-emails.png&#34; alt=&#34;Configuring an Access Policy with GitHub and email&#34; /&gt;

&lt;/p&gt;
&lt;h3 id=&#34;bypass-login-completely-when-connected-through-warp&#34;&gt;Bypass login completely when connected through WARP&lt;/h3&gt;
&lt;p&gt;Another policy I like having is to skip the login screen entirely when connected through Warp. If a user is already enrolled into my Zero Trust org and has the Warp client provisioned, then there’s no need to ask them to authenticate again.&lt;/p&gt;
&lt;p&gt;We can add a separate policy (don’t edit the one we just created above), pick the &lt;em&gt;Gateway&lt;/em&gt; selector and set it to &lt;em&gt;Allow&lt;/em&gt; or &lt;em&gt;Bypass&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./policy-gateway-auth.png&#34; alt=&#34;Policy to allow Gateway auth&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Don’t use ‘Warp’&lt;/strong&gt; - the Warp selector will match anyone that has Warp running, including the consumer 1.1.1.1 app. &lt;em&gt;Gateway&lt;/em&gt;, on the other hand, matches only if someone is connecting through your Gateway, be that DNS or a provisioned Warp client.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(The ‘Gateway’ selector is only available if the Warp client is set to allow WARP authentication identity)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Warp through Zero Trust is running on a machine: No login screen&lt;/li&gt;
&lt;li&gt;No Warp running (public access): Prompt for login screen, but only allow specific emails that authenticated through GitHub&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This setup makes it very convenient to reach homeassistant, no matter if connected through Warp or not.&lt;/p&gt;
&lt;h2 id=&#34;deploying-the-warp-client-and-enrolling-into-zero-trust&#34;&gt;Deploying the Warp client and enrolling into Zero Trust&lt;/h2&gt;
&lt;p&gt;Are you still with me?&lt;/p&gt;
&lt;p&gt;Our network is basically done. We have a login-protected &lt;code&gt;homeassistant.mydomain.com&lt;/code&gt; that routes through our tunnel into our private network and terminates at &lt;code&gt;192.168.1.3&lt;/code&gt;, and we have a direct route to &lt;code&gt;192.168.1.3&lt;/code&gt; that only works when connected with Warp.&lt;/p&gt;
&lt;p&gt;We also have login policies to make sure only specific users (logged in with GitHub and certain email addresses) can access homeassistant.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;So how do we deploy the dang Warp client?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Actually the same: We create some policies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Head to Settings -&amp;gt; Warp Client&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;Enrollment Permissions&lt;/strong&gt;, we specify the same policies for who can enroll. For example, “foo.bar@domain.com” when authenticated through GitHub is allowed to enroll. In the &lt;em&gt;Login Methods&lt;/em&gt; we can specify what login methods are available when someone tries to enroll into our Zero Trust org.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./enrollment-policy-login-methods.png&#34; alt=&#34;Login methods enroll policies&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Toggle &lt;em&gt;WARP authentication identity settings&lt;/em&gt; to make the &lt;em&gt;Gateway&lt;/em&gt; selector available in policies, effectively allowing the configured WARP client to be used as a login method.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Careful here&lt;/strong&gt;, once someone is enrolled, they are basically in your Zero Trust network through Warp. Make sure you harden this.&lt;/p&gt;
&lt;p&gt;Then, in &lt;strong&gt;Profile settings&lt;/strong&gt;, we define &lt;em&gt;how the WARP client behaves&lt;/em&gt;. These are things like protocol: MASQUE or WireGuard, service mode, what IPs and domains to exclude from WARP routing (e.g., the local network should never go through WARP), setting it to exclude or include mode and so on.&lt;/p&gt;
&lt;p&gt;Other settings I recommend setting:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Install CA to system certificate store&lt;/em&gt; - installs the Cloudflare CA certificate automatically when enrolled.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Override local interface IP&lt;/em&gt; - assigns a unique CGNAT private IP to the client. This is needed for warp-to-warp routing.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Device Posture&lt;/em&gt; - what checks the WARP client should perform for the org. E.g., check the OS version, some OS files on disk, etc. I have this set to &lt;em&gt;WARP&lt;/em&gt; and &lt;em&gt;Gateway&lt;/em&gt; because I want the client to provide information on whether the user is connected through WARP and Gateway, for skipping certain login pages.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./device-posture-checks.png&#34; alt=&#34;Device posture checks for Gateway and Warp&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Once done, just open the Warp client (&lt;a href=&#34;https://developers.cloudflare.com/warp-client/)&#34;&gt;https://developers.cloudflare.com/warp-client/)&lt;/a&gt;, and log in to your network. This should open the login pages you specified in the &lt;em&gt;Device Enrollment&lt;/em&gt; screen, and check all the enrollment policies you specified.&lt;/p&gt;
&lt;p&gt;Once passed, congratulations, your WARP client is now connected to your Zero Trust network. The client will then go ahead and start routing &lt;code&gt;192.168.1.3&lt;/code&gt; through your tunnels, as specified in your tunnel and route settings.&lt;/p&gt;
&lt;p&gt;🎉&lt;/p&gt;
&lt;h2 id=&#34;what-we-built&#34;&gt;What we built&lt;/h2&gt;
&lt;p&gt;If you followed this guide, here is what we built:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Login methods to connect the Warp client to your Zero Trust org through GitHub and specific email addresses&lt;/li&gt;
&lt;li&gt;A &lt;em&gt;tunnel&lt;/em&gt; within your private network that
&lt;ul&gt;
&lt;li&gt;Forwards any request coming in with host &lt;code&gt;homeassistant.mydomain.com&lt;/code&gt; to &lt;code&gt;192.168.1.3&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;em&gt;route&lt;/em&gt; that forwards all traffic for &lt;code&gt;192.168.1.3&lt;/code&gt; to the tunnel in your private network, which will terminate it at 192.168.1.3, which will only work when connected through Warp to route the request&lt;/li&gt;
&lt;li&gt;A DNS name &lt;code&gt;homeassistant.mydomain.com&lt;/code&gt; that points to the Argo tunnel, and will allow everyone (even if not connected through Warp) to access homeassistant which runs at 192.168.1.3&lt;/li&gt;
&lt;li&gt;Access policies that will
&lt;ul&gt;
&lt;li&gt;Ask users that are not connected to Zero Trust through Warp to log in with GitHub and specific email, so everyone can access it if they can log in&lt;/li&gt;
&lt;li&gt;A policy that skips the login screen completely and just shows homeassistant if the user connects through Zero Trust Warp client (enrolled into our org)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don&amp;rsquo;t need the public domain and you don&amp;rsquo;t need the route to 192.168.1.3. These are 2 different options that you can use to expose homeassistant when you&amp;rsquo;re not at home. One is using a public domain name everyone can see, one is explicitly requiring connecting through enrolled Warp.&lt;/p&gt;
&lt;p&gt;What I &lt;em&gt;didn&amp;rsquo;t&lt;/em&gt; cover in this post:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Warp-to-warp routing&lt;/li&gt;
&lt;li&gt;Creating and assigning fully private IPs that only exist within your Zero Trust network&lt;/li&gt;
&lt;li&gt;SSH authentication through Zero Trust access policies (that&amp;rsquo;s what we need &lt;em&gt;Targets&lt;/em&gt; for)&lt;/li&gt;
&lt;li&gt;The other application types besides &lt;em&gt;Self-Hosted&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&amp;rsquo;m happy to expand on it if there&amp;rsquo;s interest. Let me know on &lt;a href=&#34;https://x.com/dvcrn&#34;&gt;X&lt;/a&gt; or &lt;a href=&#34;https://bsky.app/profile/david.d.sh&#34;&gt;Bluesky&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Happy tunneling! ⛅&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/cloudflare-zero-trust-tunnels/banner.jpeg" /></p><p>A while ago, after frustration with Tailscale in environments where it couldn&rsquo;t properly penetrate NAT/firewall and get a p2p connection, I decided to invest some time into learning something new: Cloudflare Zero Trust + Warp.</p>
<p>There are so many new concepts, but after way too long, I can finally say that <strong>I understand Cloudflare Zero Trust Warp now</strong>. I am a full-on Cloudflare Zero Trust with Warp convert, and while I still have Tailscale running in parallel, almost everything I do now is going through Zero Trust tunnels.</p>
<p>This post is an explanation of the basic concepts, because I’m sure others will have similar issues wrapping their head around it.</p>
<h4 id="why-tho">Why tho?</h4>
<p>Why would you even sink so much time into learning this? What does it give you?</p>
<p>Argo tunnels through Zero Trust allow you to do a bunch of <em>really</em> cool things:</p>
<ul>
<li>Connect private networks together - can be home networks, can be kubernetes clusters, you can create tunnels to and from every infra</li>
<li>Expose private services to the public, on public hostnames, no matter where they are running. You could even put your router running at 192.168.1.1 on the internet, accessible to everyone, no Warp client required</li>
<li>Create fully private networks with private IPs (10.x.x.x) that only resolve when Warp is connected, to services you specify</li>
<li>Quickly expose a public route to any service running locally or on any server, for quick development, testing webhooks or giving coworkers a quick preview</li>
<li>Create a fully private network running at home that&rsquo;s only available when you&rsquo;re connected to the Warp VPN client, or only to you, reachable anywhere</li>
<li>No worries about NAT, everything goes through the Cloudflare network, no direct p2p connection required</li>
<li>Add very granular access policies on who can access what - what login method does the user need, which email addresses are allowed. Allow bots and server-to-server exceptions with service access tokens.
<ul>
<li>Does the user need to have Warp running? Does he need to be enrolled in Zero Trust? Does he need some special permission flag?</li>
</ul>
</li>
<li>Authenticate to SSH servers through Zero Trust access policies without the need of SSH keys. Just connect Warp, type <code>ssh host</code> and you&rsquo;re logged in
<ul>
<li>Close public SSH ports completely to only allow login through Warp</li>
</ul>
</li>
<li>Get the benefits of Cloudflare VPN edge routing on top (similar to 1.1.1.1 Warp+)</li>
</ul>
<h2 id="quickie-cloudflare-zero-trust-vs-tailscale">Quickie: Cloudflare Zero Trust vs Tailscale</h2>
<p>To get this out of the way:</p>
<ul>
<li><strong>Tailscale</strong>: peer-to-peer, uses NAT and firewall penetration methods to establish p2p connections. If not possible, it goes through central relay servers. Absolute best speed and latency if a connection is established.</li>
<li><strong>Cloudflare</strong>: All traffic (with the exception of warp-to-warp routing, which is p2p) goes through Cloudflare&rsquo;s edge network. So even SSH-ing into your local router will hop through Cloudflare servers. This adds latency, but no issues with NAT at all.</li>
</ul>
<h2 id="cloudflared--warp">Cloudflared != Warp</h2>
<p>Cloudflare has 2 tools available: <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/">Warp Client</a> and <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/">Cloudflared</a>. They interact with each other and have similarities in some areas but are not the same.</p>
<p><strong>Warp Client</strong></p>
<p>The tool that connects you to the Cloudflare network. This is the thing that you configure to add clients into your Zero Trust network and enforces policies.</p>
<p>Usually this runs on clients, but can also run on servers.</p>
<p>Warp client also supports <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/private-net/warp-to-warp/">warp-to-warp routing</a> which is a true p2p connection similar to Tailscale.</p>
<p><strong>Cloudflared</strong></p>
<p>The thing that creates a <strong>tunnel</strong> and adds it to the Zero Trust network.</p>
<p>Most commonly you run this on servers to expose tunnels into your network, but you can also run it on clients.</p>
<p>On the client side you can use <code>cloudflared access</code> to establish a connection with other things in your Zero Trust network.</p>
<p>Can also create one-time-use tunnels that aren’t connected to the Zero Trust network. Good for testing.</p>
<h2 id="tunnels-routes-targets">Tunnels, Routes, Targets</h2>
<p>This took me the longest to understand. Zero Trust allows you to configure <em>Tunnels</em>, <em>Routes</em> and <em>Targets</em>; here’s how they interplay.</p>
<h3 id="tunnels">Tunnels</h3>
<p>The most important part of your setup. Tunnels are deployed through <code>cloudflared</code> and are simply an <em>exit</em> for traffic. Think of it as a literal tunnel that has its end somewhere.</p>
<p>Tunnels are deployed to infrastructure in the target network. So if you have a home network with 192.168.1.1/24, you want to deploy <code>cloudflared</code> on any machine that’s always on and within that network. It can be your router, or your Raspi, it doesn&rsquo;t matter.</p>
<p>For server-hosted services, you can have a tunnel on your main dev server, on a server, or on a pod in your Kubernetes cluster.</p>
<p>Now you have an opening into these networks through Warp/Argo tunnels.</p>
<h4 id="configuring-tunnels">Configuring tunnels</h4>
<p>You can either <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/local-management/configuration-file/">configure</a> tunnels through the Zero Trust UI by “adopting” them, or configure them in the <code>/etc/cloudflared/config.yml</code> config on the machine itself. Personal preference, I usually configure them on the machine itself.</p>
<p>The config specifies where a request should get routed to when it arrives at the tunnel. So the tunnel knows what to do with it.</p>
<p>In this config we tell cloudflared to route traffic arriving at this tunnel for hostname <code>gitlab.widgetcorp.tech</code> to localhost:80, and <code>gitlab-ssh</code> to the local SSH server.</p>
<pre tabindex="0"><code>❯ cat /etc/cloudflared/config.yml
tunnel: a2f17e27-cd4d-4fcd-b02a-63839f57a96f
credentials-file: /etc/cloudflared/a2f17e27-cd4d-4fcd-b02a-63839f57a96f.json
ingress:
  - hostname: gitlab.widgetcorp.tech
    service: http://localhost:80
  - hostname: gitlab-ssh.widgetcorp.tech
    service: ssh://localhost:22
  - service: http_status:404

  # Catch-all for WARP routing
  - service: http_status:404

warp-routing:
  enabled: true
</code></pre><p>The config alone doesn’t do anything. It just exposes a tunnel, and that&rsquo;s it. What we need now are routes and targets.</p>
<h4 id="exposing-a-private-network-to-the-public-with-tunnels-quickly">Exposing a private network to the public with tunnels quickly</h4>
<p>Quick addition, as this is a super common use case. If you want to just expose something in your home network to the internet, you can add a config like this:</p>
<pre tabindex="0"><code>tunnel: a2f17e27-cd4d-4fcd-b02a-63839f57a96f
credentials-file: /etc/cloudflared/a2f17e27-cd4d-4fcd-b02a-63839f57a96f.json
ingress:
  - hostname: homeassistant.mydomain.com
    service: http://192.168.1.3:80
</code></pre><p>Then go into Cloudflare DNS settings and map the domain <code>homeassistant.mydomain.com</code> to the tunnel:</p>
<p><code>CNAME homeassistant.mydomain.com a2f17e27-cd4d-4fcd-b02a-63839f57a96f.cfargotunnel.com</code></p>
<p>Now all traffic going to this domain will go through the cloudflared tunnel, which is configured to route <code>homeassistant.mydomain.com</code> to <code>192.168.1.3</code>. <strong>No Warp client needed</strong>, Argo tunnel does everything for us.</p>
<p><strong>Note:</strong> If you adopted the tunnels and don’t use <code>config.yaml</code>, you can automatically create matching DNS records in the Cloudflare UI and don’t need to do this manually.</p>
<h3 id="routes">Routes</h3>
<p>A route defines <em>where</em> to direct traffic to.</p>
<p>Let’s say your homeassistant runs on 192.168.1.3 at home and you want to reach it from outside. Just above we deployed a <code>cloudflared</code> tunnel on our router at 192.168.1.3, and added a config pointing the domain to the Argo tunnel, so <code>homeassistant.mydomain.com</code> is already available to the public. However, <code>192.168.1.3</code> isn’t, as it’s a private network IP.</p>
<p>
<img class="img-fluid" src="routes-configure.png" alt="Configuring a route in Zero Trust" />

</p>
<p>You can define:</p>
<ul>
<li>A route like <code>192.168.1.1/24</code> pointing at your tunnel, to route <em>ALL</em> traffic to the full IP range through that tunnel (so even 192.168.1.245 will go through your tunnel)</li>
<li>Or a more specific route like <code>192.168.1.3/32</code> pointing at your tunnel, to <em>ONLY</em> route traffic to 192.168.1.3 through that tunnel.</li>
</ul>
<p>When configured, once your user connects their Warp client that&rsquo;s set up with your Zero Trust network, the Warp client will see requests to 192.168.1.3 and route it through the Cloudflare network to reach your specific tunnel. Like a little police helper directing cars where to go.</p>
<p><em>If the Warp client is <strong>not</strong> connected, 192.168.1.3 will just resolve in your current local network. If connected, it will resolve to the tunnel.</em></p>
<p><strong>The routed IP doesn’t need to exist!</strong> So you could, for example, route a random IP you like (e.g., 10.128.1.1) to your tunnel, the tunnel then forwards it based on your routes, for example to 192.168.1.1. This is extremely powerful because it allows you to build your own fully virtual network.</p>
<p>That’s all it does, what happens afterwards is up to the tunnel config that we created above. The tunnel decides where to point the incoming request to, whether that’s localhost or somewhere else.</p>
<p>To summarize, the <code>route</code> tells the Warp client where to route traffic to.</p>
<p>Now we have 2 things working:</p>
<ul>
<li><code>homeassistant.mydomain.com</code> - goes through a Cloudflare DNS record pointing at an Argo tunnel, which then forwards to 192.168.1.3. This works without Warp connected as it&rsquo;s on the DNS level, public to everyone.</li>
<li><code>192.168.1.3</code> - The Warp client sees the request and routes it through the Argo tunnel, which then forwards it to <code>192.168.1.3</code> within that network. This needs Warp connected to work, and is only visible to people in your Zero Trust org.</li>
</ul>
<h3 id="targets">Targets</h3>
<p>This one took me a while.</p>
<p>Targets are needed to <em>define a piece of infrastructure</em> that you want to protect through Zero Trust. They are like a pointer pointing to something in your network. This goes hand-in-hand with routes, but isn’t always needed.</p>
<p>Let’s say you have 192.168.1.3 (homeassistant) exposed through a Cloudflare tunnel. By default, anyone in your network that is part of your Zero Trust org and has Warp client installed can now access your homeassistant at 192.168.1.3.</p>
<p>We can change that with targets. For example, defining a target with hostname = <code>homeassistant.mydomain.com</code> to the route <code>192.168.1.3/32</code> allows us to add access policies to it. We can also put an entire network into the target by specifying <code>192.168.1.3/24</code> to control access. This also works with virtual IPs like 10.128.1.1!</p>
<p>
<img class="img-fluid" src="./targets-config-screen.png" alt="Configuring a target in Zero Trust" />

</p>
<p>Targets alone won’t do anything, they just point to the service or network. “Hey, here is homeassistant”, or “hey, here is my home network”.</p>
<h2 id="access-policies-protecting-who-can-access-what">Access Policies: Protecting Who Can Access What</h2>
<p>Continuing the example from above:</p>
<ul>
<li>we have a tunnel running on our home network that routes <code>homeassistant.mydomain.com</code> to <code>192.168.1.3</code></li>
<li>we set up public DNS records to point <code>homeassistant.mydomain.com</code> to the Argo tunnel in Cloudflare</li>
<li>we created a <em>route</em> <code>192.168.1.3</code> to go through the same tunnel</li>
<li>we also created a <em>target</em> pointing to <code>192.168.1.3</code></li>
</ul>
<p>When users access either <code>192.168.1.3</code> or <code>homeassistant.mydomain.com</code>, the Warp client will route the request through the tunnel, which then forwards the request to 192.168.1.3. Homeassistant loads and everything is fine.</p>
<p><em>But do we want that?</em></p>
<p>Probably not.</p>
<p>Access policies to the rescue!</p>
<p>With access policies, we can leave things in the public but protect them with Cloudflare Zero Trust access. So while 192.168.1.3 is only available if Warp is connected (so routing to it works), we can add security to our public <code>homeassistant.mydomain.com</code>.</p>
<p>Go to Access -&gt; Applications -&gt; Add an Application -&gt; Self-hosted.</p>
<p>Here we can define <em>what</em> should be protected, and <em>how</em>.</p>
<p>Going with our previous example, we can add a public hostname <code>homeassistant.mydomain.com</code> or an IP like <code>192.168.1.3</code> (or both), then attach policies of who should be able to access it.</p>
<p>You can specify <strong>Include</strong> (“OR”) and <strong>Require</strong> (“AND”) selectors.</p>
<ul>
<li><strong>Require</strong> rules must always be met, <em>on top</em> of include rules, to grant access</li>
<li>Any of the <strong>Include</strong> rules must match to grant access</li>
</ul>
<p>Then there are <em>Actions</em>:</p>
<ul>
<li><strong>Allow</strong> - when the policy matches, allow access</li>
<li><strong>Deny</strong> - when the policy matches, deny access. aka blocking something.</li>
<li><strong>Bypass</strong> - when the policy matches, bypass Zero Trust completely. No more checking.</li>
<li><strong>Service Auth</strong> - when the policy matches, allow authentication to the service with a service token header (good for server-to-server, or bots). Check Access -&gt; Service Auth to create these tokens.</li>
</ul>
<h3 id="allow-public-access-to-everyone-logging-into-your-network">Allow public access to everyone logging into your network</h3>
<p>The most common use case: <code>homeassistant.mydomain.com</code> is public. We want to keep it public, but add an extra layer of security.</p>
<p>Add an <em>include</em> policy, pick any of the <code>email</code> selectors, add the email of the user you want to allow access to. Now only people authenticated with your Zero Trust org with the specified emails can access your homeassistant, without needing to have Warp running.</p>
<p>We can harden this by adding <em>require</em> rules: Add a <em>Login Method</em> selector rule, pick a specific login method like GitHub. Now only people with specific emails that have authenticated through GitHub can access your homeassistant, without needing to have Warp running.</p>
<p>
<img class="img-fluid" src="./policy-github-emails.png" alt="Configuring an Access Policy with GitHub and email" />

</p>
<h3 id="bypass-login-completely-when-connected-through-warp">Bypass login completely when connected through WARP</h3>
<p>Another policy I like having is to skip the login screen entirely when connected through Warp. If a user is already enrolled into my Zero Trust org and has the Warp client provisioned, then there’s no need to ask them to authenticate again.</p>
<p>We can add a separate policy (don’t edit the one we just created above), pick the <em>Gateway</em> selector and set it to <em>Allow</em> or <em>Bypass</em>.</p>
<p>
<img class="img-fluid" src="./policy-gateway-auth.png" alt="Policy to allow Gateway auth" />

</p>
<p><strong>Don’t use ‘Warp’</strong> - the Warp selector will match anyone that has Warp running, including the consumer 1.1.1.1 app. <em>Gateway</em>, on the other hand, matches only if someone is connecting through your Gateway, be that DNS or a provisioned Warp client.</p>
<p><em>(The ‘Gateway’ selector is only available if the Warp client is set to allow WARP authentication identity)</em></p>
<p>Now when:</p>
<ul>
<li>Warp through Zero Trust is running on a machine: No login screen</li>
<li>No Warp running (public access): Prompt for login screen, but only allow specific emails that authenticated through GitHub</li>
</ul>
<p>This setup makes it very convenient to reach homeassistant, no matter if connected through Warp or not.</p>
<h2 id="deploying-the-warp-client-and-enrolling-into-zero-trust">Deploying the Warp client and enrolling into Zero Trust</h2>
<p>Are you still with me?</p>
<p>Our network is basically done. We have a login-protected <code>homeassistant.mydomain.com</code> that routes through our tunnel into our private network and terminates at <code>192.168.1.3</code>, and we have a direct route to <code>192.168.1.3</code> that only works when connected with Warp.</p>
<p>We also have login policies to make sure only specific users (logged in with GitHub and certain email addresses) can access homeassistant.</p>
<p><em>So how do we deploy the dang Warp client?</em></p>
<p>Actually the same: We create some policies.</p>
<p><strong>Head to Settings -&gt; Warp Client</strong></p>
<p>In <strong>Enrollment Permissions</strong>, we specify the same policies for who can enroll. For example, “foo.bar@domain.com” when authenticated through GitHub is allowed to enroll. In the <em>Login Methods</em> we can specify what login methods are available when someone tries to enroll into our Zero Trust org.</p>
<p>
<img class="img-fluid" src="./enrollment-policy-login-methods.png" alt="Login methods enroll policies" />

</p>
<p>Toggle <em>WARP authentication identity settings</em> to make the <em>Gateway</em> selector available in policies, effectively allowing the configured WARP client to be used as a login method.</p>
<p><strong>Careful here</strong>, once someone is enrolled, they are basically in your Zero Trust network through Warp. Make sure you harden this.</p>
<p>Then, in <strong>Profile settings</strong>, we define <em>how the WARP client behaves</em>. These are things like protocol: MASQUE or WireGuard, service mode, what IPs and domains to exclude from WARP routing (e.g., the local network should never go through WARP), setting it to exclude or include mode and so on.</p>
<p>Other settings I recommend setting:</p>
<ul>
<li><em>Install CA to system certificate store</em> - installs the Cloudflare CA certificate automatically when enrolled.</li>
<li><em>Override local interface IP</em> - assigns a unique CGNAT private IP to the client. This is needed for warp-to-warp routing.</li>
<li><em>Device Posture</em> - what checks the WARP client should perform for the org. E.g., check the OS version, some OS files on disk, etc. I have this set to <em>WARP</em> and <em>Gateway</em> because I want the client to provide information on whether the user is connected through WARP and Gateway, for skipping certain login pages.</li>
</ul>
<p>
<img class="img-fluid" src="./device-posture-checks.png" alt="Device posture checks for Gateway and Warp" />

</p>
<p>Once done, just open the Warp client (<a href="https://developers.cloudflare.com/warp-client/)">https://developers.cloudflare.com/warp-client/)</a>, and log in to your network. This should open the login pages you specified in the <em>Device Enrollment</em> screen, and check all the enrollment policies you specified.</p>
<p>Once passed, congratulations, your WARP client is now connected to your Zero Trust network. The client will then go ahead and start routing <code>192.168.1.3</code> through your tunnels, as specified in your tunnel and route settings.</p>
<p>🎉</p>
<h2 id="what-we-built">What we built</h2>
<p>If you followed this guide, here is what we built:</p>
<ul>
<li>Login methods to connect the Warp client to your Zero Trust org through GitHub and specific email addresses</li>
<li>A <em>tunnel</em> within your private network that
<ul>
<li>Forwards any request coming in with host <code>homeassistant.mydomain.com</code> to <code>192.168.1.3</code></li>
</ul>
</li>
<li>A <em>route</em> that forwards all traffic for <code>192.168.1.3</code> to the tunnel in your private network, which will terminate it at 192.168.1.3, which will only work when connected through Warp to route the request</li>
<li>A DNS name <code>homeassistant.mydomain.com</code> that points to the Argo tunnel, and will allow everyone (even if not connected through Warp) to access homeassistant which runs at 192.168.1.3</li>
<li>Access policies that will
<ul>
<li>Ask users that are not connected to Zero Trust through Warp to log in with GitHub and specific email, so everyone can access it if they can log in</li>
<li>A policy that skips the login screen completely and just shows homeassistant if the user connects through Zero Trust Warp client (enrolled into our org)</li>
</ul>
</li>
</ul>
<p>You don&rsquo;t need the public domain and you don&rsquo;t need the route to 192.168.1.3. These are 2 different options that you can use to expose homeassistant when you&rsquo;re not at home. One is using a public domain name everyone can see, one is explicitly requiring connecting through enrolled Warp.</p>
<p>What I <em>didn&rsquo;t</em> cover in this post:</p>
<ul>
<li>Warp-to-warp routing</li>
<li>Creating and assigning fully private IPs that only exist within your Zero Trust network</li>
<li>SSH authentication through Zero Trust access policies (that&rsquo;s what we need <em>Targets</em> for)</li>
<li>The other application types besides <em>Self-Hosted</em></li>
</ul>
<p>I&rsquo;m happy to expand on it if there&rsquo;s interest. Let me know on <a href="https://x.com/dvcrn">X</a> or <a href="https://bsky.app/profile/david.d.sh">Bluesky</a>.</p>
<p>Happy tunneling! ⛅</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Release: Quickshot AI Editor for iOS</title>
      <link>https://david.coffee/quickshot-ai-editor-release/</link>
      <pubDate>Mon, 16 Jun 2025 16:00:00 +0900</pubDate>
      
      <guid>https://david.coffee/quickshot-ai-editor-release/</guid><media:content url="https://david.coffee/quickshot-ai-editor-release/banner.png" medium="image" />
          <enclosure url="https://david.coffee/quickshot-ai-editor-release/banner.png" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/quickshot-ai-editor-release/banner.png&#34; /&gt;&lt;/p&gt;&lt;p&gt;I&amp;rsquo;ve been working on a new app that has recently been released: &lt;strong&gt;Quickshot&lt;/strong&gt;! It&amp;rsquo;s available on iOS on the AppStore at &lt;a href=&#34;https://apps.apple.com/jp/app/quickshot-ai-editor/id6744357427?l=en-US&#34;&gt;https://apps.apple.com/jp/app/quickshot-ai-editor/id6744357427?l=en-US&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;editing-photos-with-words&#34;&gt;Editing photos with words&lt;/h2&gt;
&lt;p&gt;Quickshot is an experimental photo editor powered by AI, that aims to make small photo edits quick, natural and fun - instead of using tools and apps to erase something, add something or compose something new, in Quickshot you just use your words.&lt;/p&gt;
&lt;p&gt;Quickshot lets you add, remove or manipulate images, transform scenes, or even re-style and repaint entire photos through hand-crafted presets, or by typing text yourself.&lt;/p&gt;
&lt;img src=&#34;./quickshot-start.PNG&#34; alt=&#34;IMG_4622&#34; style=&#34;zoom: 33%;&#34; /&gt;
&lt;p&gt;Some of the presets are meant to be a bit goofy and funny, but they demonstrate the app&amp;rsquo;s power quite well.&lt;/p&gt;
&lt;p&gt;For example, some of the accessories Quickshot has available as presets:&lt;/p&gt;
&lt;img src=&#34;./preset-overview.PNG&#34; alt=&#34;IMG_4623&#34; style=&#34;zoom:33%;&#34; /&gt;
&lt;p&gt;Tapping a preset will send the request to the Quickshot server which then composes an AI pipeline to figure out exactly how the image should get transformed to get the requested edit.&lt;/p&gt;
&lt;img src=&#34;./sunglasses.PNG&#34; alt=&#34;IMG_4626&#34; style=&#34;zoom:33%;&#34; /&gt;
&lt;p&gt;There&amp;rsquo;s a lot of magic happening behind the scenes, but once the details are figured out, the app will give you within roughly 10 seconds a new image, now with your change:&lt;/p&gt;
&lt;p&gt;Now with the new edited image loaded into the editor, we can continue transforming it further and further, until we&amp;rsquo;re happy with the final result.&lt;/p&gt;
&lt;h2 id=&#34;more-than-just-presets-natural-language&#34;&gt;More than just presets: Natural Language&lt;/h2&gt;
&lt;p&gt;Presets aren&amp;rsquo;t everything, you can also use Quickshot with natural language.&lt;/p&gt;
&lt;img src=&#34;./text-input-example.PNG&#34; alt=&#34;IMG_4627&#34; style=&#34;zoom:33%;&#34; /&gt;
&lt;p&gt;Same thing here - the Quickshot AI pipeline will figure out all the details of what to do to make this change a reality, then uses state-of-the-art image models to compose the final result.&lt;/p&gt;
&lt;p&gt;In the example above, the result would be:&lt;/p&gt;
&lt;img src=&#34;./lego-result.PNG&#34; alt=&#34;IMG_4628&#34; style=&#34;zoom:33%;&#34; /&gt;
&lt;h2 id=&#34;11-now-on-ios&#34;&gt;1.1 now on iOS!&lt;/h2&gt;
&lt;p&gt;Version 1.1 is now available on the AppStore. You can install it from &lt;a href=&#34;https://apps.apple.com/jp/app/quickshot-ai-editor/id6744357427?l=en-US&#34;&gt;https://apps.apple.com/jp/app/quickshot-ai-editor/id6744357427?l=en-US&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;3 edits per day are free, more than that and the app will ask you for a small subscription to keep the lights on.&lt;/p&gt;
&lt;p&gt;I had a lot of fun building this and started using the app a lot myself. I&amp;rsquo;m especially a fan of the stronger crazy transformations like turning yourself into an action figure 😆&lt;/p&gt;
&lt;img src=&#34;./actionfigure.jpeg&#34; alt=&#34;Action Figure of myself&#34; style=&#34;zoom:33%;&#34; /&gt;
&lt;p&gt;I have a lot of cool things planned for this app. Give it a try and let me know what you think!&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/quickshot-ai-editor-release/banner.png" /></p><p>I&rsquo;ve been working on a new app that has recently been released: <strong>Quickshot</strong>! It&rsquo;s available on iOS on the AppStore at <a href="https://apps.apple.com/jp/app/quickshot-ai-editor/id6744357427?l=en-US">https://apps.apple.com/jp/app/quickshot-ai-editor/id6744357427?l=en-US</a></p>
<h2 id="editing-photos-with-words">Editing photos with words</h2>
<p>Quickshot is an experimental photo editor powered by AI, that aims to make small photo edits quick, natural and fun - instead of using tools and apps to erase something, add something or compose something new, in Quickshot you just use your words.</p>
<p>Quickshot lets you add, remove or manipulate images, transform scenes, or even re-style and repaint entire photos through hand-crafted presets, or by typing text yourself.</p>
<img src="./quickshot-start.PNG" alt="IMG_4622" style="zoom: 33%;" />
<p>Some of the presets are meant to be a bit goofy and funny, but they demonstrate the app&rsquo;s power quite well.</p>
<p>For example, some of the accessories Quickshot has available as presets:</p>
<img src="./preset-overview.PNG" alt="IMG_4623" style="zoom:33%;" />
<p>Tapping a preset will send the request to the Quickshot server which then composes an AI pipeline to figure out exactly how the image should get transformed to get the requested edit.</p>
<img src="./sunglasses.PNG" alt="IMG_4626" style="zoom:33%;" />
<p>There&rsquo;s a lot of magic happening behind the scenes, but once the details are figured out, the app will give you within roughly 10 seconds a new image, now with your change:</p>
<p>Now with the new edited image loaded into the editor, we can continue transforming it further and further, until we&rsquo;re happy with the final result.</p>
<h2 id="more-than-just-presets-natural-language">More than just presets: Natural Language</h2>
<p>Presets aren&rsquo;t everything, you can also use Quickshot with natural language.</p>
<img src="./text-input-example.PNG" alt="IMG_4627" style="zoom:33%;" />
<p>Same thing here - the Quickshot AI pipeline will figure out all the details of what to do to make this change a reality, then uses state-of-the-art image models to compose the final result.</p>
<p>In the example above, the result would be:</p>
<img src="./lego-result.PNG" alt="IMG_4628" style="zoom:33%;" />
<h2 id="11-now-on-ios">1.1 now on iOS!</h2>
<p>Version 1.1 is now available on the AppStore. You can install it from <a href="https://apps.apple.com/jp/app/quickshot-ai-editor/id6744357427?l=en-US">https://apps.apple.com/jp/app/quickshot-ai-editor/id6744357427?l=en-US</a></p>
<p>3 edits per day are free, more than that and the app will ask you for a small subscription to keep the lights on.</p>
<p>I had a lot of fun building this and started using the app a lot myself. I&rsquo;m especially a fan of the stronger crazy transformations like turning yourself into an action figure 😆</p>
<img src="./actionfigure.jpeg" alt="Action Figure of myself" style="zoom:33%;" />
<p>I have a lot of cool things planned for this app. Give it a try and let me know what you think!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Better Vibe Coding - Part 2, Multi-agents &amp; Complex Tasks</title>
      <link>https://david.coffee/vibe-coding-advanced/</link>
      <pubDate>Tue, 03 Jun 2025 10:00:00 +0900</pubDate>
      
      <guid>https://david.coffee/vibe-coding-advanced/</guid><media:content url="https://david.coffee/vibe-coding-advanced/vibebot.jpeg" medium="image" />
          <enclosure url="https://david.coffee/vibe-coding-advanced/vibebot.jpeg" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/vibe-coding-advanced/vibebot.jpeg&#34; /&gt;&lt;/p&gt;&lt;p&gt;This post is a direct continuation of &lt;a href=&#34;https://david.coffee/vibe-coding-essentials/&#34;&gt;Better Vibe Coding - Part 1, Foundation &amp;amp; Basics&lt;/a&gt;. If you haven’t read that yet, start there for the basics.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;2-vibe-coding-complex-tasks&#34;&gt;2. &amp;ldquo;Vibe Coding&amp;rdquo; complex tasks&lt;/h2&gt;
&lt;p&gt;Phew, now that we got the basics covered, let’s go into more advanced territory.&lt;/p&gt;
&lt;p&gt;If you vibe coded in bigger codebases you’ll likely face issues with the agent&amp;rsquo;s quality going down the drain the more files it reads, and the more complex a task becomes.&lt;/p&gt;
&lt;p&gt;When an agent reads in more tokens, eventually it’ll flush out stuff that came in the beginning of the conversation and lose context. We simply can’t afford to read files that we don’t need to read, and features that span multiple files need different handling.&lt;/p&gt;
&lt;p&gt;All of these things I mention here is what I use for my projects on a daily basis and consider crucial.&lt;/p&gt;
&lt;h3 id=&#34;21-split-plan-phases-from-implementation-phases&#34;&gt;2.1 Split plan phases from implementation phases&lt;/h3&gt;
&lt;p&gt;Putting it as it is: Purely vibe coding complex features (as in, opening a prompt and telling an agent to &amp;ldquo;implement this feature&amp;rdquo;) is just not doable currently due to limitations mentioned previously.&lt;/p&gt;
&lt;p&gt;Imagine treating a new-joiner in your team like an AI agent. Would you just tell them to &amp;ldquo;implement this feature&amp;rdquo; without any guidance? Yeah very likely not (unless you want to scare your new hire away already).&lt;/p&gt;
&lt;p&gt;So how can we work around it? By thinking like a manager: Separating planning from implementation phases.&lt;/p&gt;
&lt;p&gt;Part of implementing any feature in a professional context always involves a planning phase before we can create use stories or a design document, so why should working with agents be any different?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Planning phase&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Understand and clarify the ask: What do we need to do, why&lt;/li&gt;
&lt;li&gt;Define the acceptance criteria&lt;/li&gt;
&lt;li&gt;Read a lot of code&lt;/li&gt;
&lt;li&gt;Research&lt;/li&gt;
&lt;li&gt;Come up with a plan&lt;/li&gt;
&lt;li&gt;Output a design document&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Implementation phase&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Often a &lt;strong&gt;new&lt;/strong&gt; session to flush the context&lt;/li&gt;
&lt;li&gt;Reads the plan and implements it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the planning phase, I use a model with a bigger context since it has to gobble up a lot of code (gemini-2.5-pro or GPT4.1). This is where &lt;em&gt;thinking-models&lt;/em&gt; really shine and should be used: Devise a good plan, doesn’t matter how long it takes.&lt;/p&gt;
&lt;p&gt;For the implementation phase, thinking is usually not required because the heavy lifting has already been done in the planning phase.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./flow.png&#34; alt=&#34;approach diagram&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://docs.roocode.com&#34;&gt;Roo&lt;/a&gt; or &lt;a href=&#34;https://cline.bot&#34;&gt;Cline&lt;/a&gt; have &lt;em&gt;PLAN&lt;/em&gt; phases built-in, but we can easily emulate that in other tools as well with a prompt like the following (I have this as a custom Cursor mode):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;You are currently in PLAN mode.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; PLAN mode is for creating detailed plans and strategies, not for implementing code changes
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Your goal is to gather information, conduct research, and develop a comprehensive plan before any implementation
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Focus on understanding requirements and architecting solutions rather than writing actual code
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Use tools like &lt;span style=&#34;color:#e6db74&#34;&gt;`read_file`&lt;/span&gt; and &lt;span style=&#34;color:#e6db74&#34;&gt;`search_files`&lt;/span&gt; to gather context about the existing codebase
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Ask clarifying questions to ensure you fully understand what&amp;#39;s needed
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Create detailed, well-researched plans with clear steps and reasoning
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Use Mermaid diagrams when helpful to visualize architecture, workflows, or processes
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; You may include small code snippets to illustrate concepts, but avoid suggesting full implementations
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Engage in back-and-forth discussion to refine the plan until it meets requirements
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; When a satisfactory plan is established, suggest switching to ACT mode for implementation
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Remember: in PLAN mode, your focus is exclusively on planning, not doing
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PREFIX ANY MESSAGE YOU WRITE WITH &amp;#34;PLAN MODE:&amp;#34; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;THIS IS VERY VERY IMPORTANT!!!
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;If you get an error of edit_file not being available, then THIS IS A REMINDER TO YOU THAT WE ARE IN PLAN MODE!!!! REMEMBER YOUR INSTRUCTIONS
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once the plan is generated, I pass it to a new agent to do the actual implementation.&lt;/p&gt;
&lt;h3 id=&#34;22-resumption-and-hand-off-with-design-docs-and-logs&#34;&gt;2.2 Resumption and hand-off with design docs and logs&lt;/h3&gt;
&lt;p&gt;Smaller edits can be made one-shot by passing the plan to an implementor model and letting it do its thing, but bigger plans may still exceed the context window and drop in quality.&lt;/p&gt;
&lt;p&gt;For this reason, I always use 2 document formats: A &lt;strong&gt;clearly defined design doc&lt;/strong&gt; and a &lt;strong&gt;log&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The design doc is the output of the planning phase. Its purpose is similar to the “onboarding document” we talked about previously, but with the objective to onboard someone onto a specific feature, so the same rules apply: Include everything that someone with no knowledge should know about this feature:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What needs to be done and why&lt;/li&gt;
&lt;li&gt;Link to documents, tickets&lt;/li&gt;
&lt;li&gt;Technical design&lt;/li&gt;
&lt;li&gt;Clearly defined list with proposed high-level changes
&lt;ul&gt;
&lt;li&gt;What files need to be looked at, where to find them&lt;/li&gt;
&lt;li&gt;What changes do we need to do in those files&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The purpose of this document is to condense the huge context window research that the planning phase did into something that is actionable, and more important: &lt;em&gt;reviewable (by us)&lt;/em&gt;. &lt;strong&gt;This is the final big step before implementation, so we need to make sure that the idea the LLM has aligns with what we want to do.&lt;/strong&gt; I iterate on these design docs a couple times until it&amp;rsquo;s in a shape that I&amp;rsquo;m happy with.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Always&lt;/strong&gt; define a guide or template for these documents. Don’t let the AI do what it wants, we want the documents to be consistent and contain the information we already know we need for effective implementation.&lt;/p&gt;
&lt;p&gt;A good format (from my experience) is following similar principles to the rules and manual document. This document is not intended for humans, it&amp;rsquo;s for LLMs, so it needs to be able to quickly bring the agent up to speed and point it towards the files and important bits that it needs to know, while providing context to do additional research if needed.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;# Agent UI Simplification Design Doc
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold&#34;&gt;**Date:**&lt;/span&gt; 2025-04-24
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold&#34;&gt;**Author:**&lt;/span&gt; GitHub Copilot
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold&#34;&gt;**Linear Issue:**&lt;/span&gt; MIC-28
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;## 1. Overview
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Simplify the Agent interface by merging the configuration options (currently under a separate &amp;#34;Settings&amp;#34; tab) into the main &amp;#34;Execute&amp;#34; tab. Rename the combined tab to &amp;#34;Agent&amp;#34;. The &amp;#34;Execution History&amp;#34; tab will remain separate.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;## 2. Motivation
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;The current tab structure (Execute, Settings, Execution History) with nested tabs (Configuration, Functions) within Settings is convoluted. Users need to switch tabs frequently to configure and run an agent. Combining configuration and execution into a single view streamlines the workflow.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;## 3. Proposed Changes
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt;   &lt;span style=&#34;font-weight:bold&#34;&gt;**Modify `app/agents/[id]/page.tsx`**&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt;   Remove the &amp;#34;Settings&amp;#34; &lt;span style=&#34;color:#e6db74&#34;&gt;`TabsTrigger`&lt;/span&gt; and &lt;span style=&#34;color:#e6db74&#34;&gt;`TabsContent`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt;   Rename the &amp;#34;Execute&amp;#34; &lt;span style=&#34;color:#e6db74&#34;&gt;`TabsTrigger`&lt;/span&gt; to &amp;#34;Agent&amp;#34;.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt;   &lt;span style=&#34;font-weight:bold&#34;&gt;**Modify `components/agents/agent-execution-panel.tsx`**&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt;   Integrate the UI elements and logic currently present in &lt;span style=&#34;color:#e6db74&#34;&gt;`components/agents/agent-config-panel.tsx`&lt;/span&gt; (Agent Name, Description, Model, System Prompt, Function Linking).
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt;   Arrange the configuration elements logically within the panel, likely above or alongside the execution controls.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt;   &lt;span style=&#34;font-weight:bold&#34;&gt;**Deprecate/Remove `components/agents/agent-config-panel.tsx`**&lt;/span&gt;: This component will no longer be needed as its functionality is merged into the execution panel.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;## 4. Technical Design
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;1.&lt;/span&gt;  &lt;span style=&#34;font-weight:bold&#34;&gt;**Update Tabs in `AgentPage`**&lt;/span&gt;: Modify the &lt;span style=&#34;color:#e6db74&#34;&gt;`Tabs`&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;`TabsList`&lt;/span&gt;, and &lt;span style=&#34;color:#e6db74&#34;&gt;`TabsContent`&lt;/span&gt; structure in &lt;span style=&#34;color:#e6db74&#34;&gt;`app/agents/[id]/page.tsx`&lt;/span&gt; to reflect the two-tab layout (&amp;#34;Agent&amp;#34;, &amp;#34;Execution History&amp;#34;).
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;2.&lt;/span&gt;  &lt;span style=&#34;font-weight:bold&#34;&gt;**Merge Components**&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt;   Identify the specific UI sections and state management logic within &lt;span style=&#34;color:#e6db74&#34;&gt;`AgentConfigPanel`&lt;/span&gt; (e.g., form handling for name/description, model selection, system prompt editor, function linking UI).
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt;   Transfer these UI sections and their associated logic (props, state, handlers) into &lt;span style=&#34;color:#e6db74&#34;&gt;`AgentExecutionPanel`&lt;/span&gt;. Ensure props (&lt;span style=&#34;color:#e6db74&#34;&gt;`agent`&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;`functions`&lt;/span&gt;) are correctly utilized.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt;   Adjust the layout within &lt;span style=&#34;color:#e6db74&#34;&gt;`AgentExecutionPanel`&lt;/span&gt; to accommodate the new configuration elements. A vertical stack (Config section above Execution section) seems simplest initially.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;3.&lt;/span&gt;  &lt;span style=&#34;font-weight:bold&#34;&gt;**Refactor/Remove `AgentConfigPanel`**&lt;/span&gt;: Once all functionality is migrated, &lt;span style=&#34;color:#e6db74&#34;&gt;`AgentConfigPanel`&lt;/span&gt; can be removed or refactored if any parts are still reusable independently (unlikely).
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;4.&lt;/span&gt;  &lt;span style=&#34;font-weight:bold&#34;&gt;**Testing**&lt;/span&gt;: (Future step) Add/update tests for &lt;span style=&#34;color:#e6db74&#34;&gt;`AgentExecutionPanel`&lt;/span&gt; to cover the integrated configuration options.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The counterpart of the design doc is the work log - a document the agent needs to create after it is done. This document references the design doc and explains what the agent actually did.&lt;/p&gt;
&lt;p&gt;The purpose of this document is to glance at what has been done, but also to potentially feed into a new agent to get up to speed on this feature, to do additional changes.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;# Handoff: Agent UI Gamification - 2025-04-24
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold&#34;&gt;**Author:**&lt;/span&gt; GitHub Copilot
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;## TL;DR
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;We&amp;#39;re redesigning the Agent configuration screen (&lt;span style=&#34;color:#e6db74&#34;&gt;`app/agents/[id]/page.tsx`&lt;/span&gt;) to be more visual and intuitive, replacing the old text-heavy forms with a &amp;#34;gamified&amp;#34; interface centered around a robot avatar.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold&#34;&gt;**Design Doc:**&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`docs/designdocs/agent-gamified-ui.md`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;## What We Did So Far
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;1.&lt;/span&gt;  &lt;span style=&#34;font-weight:bold&#34;&gt;**Created `AgentVisualConfigurator` Component:**&lt;/span&gt; Built a new component (&lt;span style=&#34;color:#e6db74&#34;&gt;`components/agents/agent-visual-configurator.tsx`&lt;/span&gt;) that displays:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; A robot avatar (&lt;span style=&#34;color:#e6db74&#34;&gt;`BotIcon`&lt;/span&gt;).
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; The agent&amp;#39;s objective/system prompt (currently display-only).
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Visual &amp;#34;Skill Slots&amp;#34; for linked functions.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;2.&lt;/span&gt;  &lt;span style=&#34;font-weight:bold&#34;&gt;**Integrated into Agent Page:**&lt;/span&gt; Replaced the old configuration cards in &lt;span style=&#34;color:#e6db74&#34;&gt;`components/agents/agent-execution-panel.tsx`&lt;/span&gt; with the new &lt;span style=&#34;color:#e6db74&#34;&gt;`AgentVisualConfigurator`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;3.&lt;/span&gt;  &lt;span style=&#34;font-weight:bold&#34;&gt;**Skill Slot Functionality:**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; **Display:** Filled slots show the function name (with &lt;span style=&#34;color:#e6db74&#34;&gt;`SwordIcon`&lt;/span&gt; 🗡️) and workspace.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; **Add:** Clicking an empty slot (&lt;span style=&#34;color:#e6db74&#34;&gt;`+`&lt;/span&gt;) opens the &lt;span style=&#34;color:#e6db74&#34;&gt;`FunctionSearch`&lt;/span&gt; dialog to find and link a new function.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; **Edit:** Clicking a &lt;span style=&#34;font-style:italic&#34;&gt;_filled_&lt;/span&gt; slot opens the &lt;span style=&#34;color:#e6db74&#34;&gt;`FunctionLinkForm`&lt;/span&gt; modal, allowing the user to edit the description (when the agent should use this function).
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; **Remove:** Clicking the trash icon (&lt;span style=&#34;color:#e6db74&#34;&gt;`Trash2Icon`&lt;/span&gt;) on a filled slot removes the function link.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;4.&lt;/span&gt;  &lt;span style=&#34;font-weight:bold&#34;&gt;**Data Fetching:**&lt;/span&gt; Updated React Query mutation hooks (&lt;span style=&#34;color:#e6db74&#34;&gt;`lib/hooks/use-agent-functions.ts`&lt;/span&gt;) to &lt;span style=&#34;color:#e6db74&#34;&gt;`await`&lt;/span&gt; query invalidation on success, ensuring the UI reflects changes immediately after adding/editing/removing functions without needing a page refresh.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;5.&lt;/span&gt;  &lt;span style=&#34;font-weight:bold&#34;&gt;**Styling:**&lt;/span&gt; Applied basic Tailwind CSS and shadcn UI styling to the new component and slots.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;## Current State
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; The visual layout is in place on the agent page.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Users can view the agent&amp;#39;s objective.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Users can add, edit the description of, and remove function links via the skill slots and associated modals (&lt;span style=&#34;color:#e6db74&#34;&gt;`FunctionSearch`&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;`FunctionLinkForm`&lt;/span&gt;).
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; The underlying data updates correctly and the UI refreshes thanks to query invalidation.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;## Next Steps / What&amp;#39;s Left
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;1.&lt;/span&gt;  &lt;span style=&#34;font-weight:bold&#34;&gt;**Save Objective:**&lt;/span&gt; Hook up the objective/system prompt &lt;span style=&#34;color:#e6db74&#34;&gt;`textarea`&lt;/span&gt; in &lt;span style=&#34;color:#e6db74&#34;&gt;`AgentVisualConfigurator`&lt;/span&gt; to actually save changes (likely using &lt;span style=&#34;color:#e6db74&#34;&gt;`useUpdateAgent`&lt;/span&gt;).
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;2.&lt;/span&gt;  &lt;span style=&#34;font-weight:bold&#34;&gt;**Run Agent Integration:**&lt;/span&gt; Connect the &amp;#34;Try Your Agent&amp;#34; input and button (&lt;span style=&#34;color:#e6db74&#34;&gt;`AgentExecutionPanel`&lt;/span&gt;) to work with this new view (it might already work, but needs verification).
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;3.&lt;/span&gt;  &lt;span style=&#34;font-weight:bold&#34;&gt;**Execution History:**&lt;/span&gt; Implement the display area for execution logs below the configurator.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;4.&lt;/span&gt;  &lt;span style=&#34;font-weight:bold&#34;&gt;**Testing:**&lt;/span&gt; Write tests for &lt;span style=&#34;color:#e6db74&#34;&gt;`AgentVisualConfigurator`&lt;/span&gt; and potentially update tests for &lt;span style=&#34;color:#e6db74&#34;&gt;`AgentExecutionPanel`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;5.&lt;/span&gt;  &lt;span style=&#34;font-weight:bold&#34;&gt;**Refinement (Optional):**&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Add subtle animations (e.g., robot idle, feedback on add/remove).
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;-&lt;/span&gt; Further styling improvements.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;6.&lt;/span&gt;  &lt;span style=&#34;font-weight:bold&#34;&gt;**Final Build:**&lt;/span&gt; Run &lt;span style=&#34;color:#e6db74&#34;&gt;`bun run build`&lt;/span&gt; to ensure everything builds correctly.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;7.&lt;/span&gt;  &lt;span style=&#34;font-weight:bold&#34;&gt;**Documentation:**&lt;/span&gt; Update any relevant user-facing docs if needed.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;This should get the next person up to speed! Lmk if you need anything else. 🔥
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;23-multi-agent-workflow-with-agent-to-agent-delegation&#34;&gt;2.3 Multi-agent workflow with agent-to-agent delegation&lt;/h3&gt;
&lt;p&gt;Now we&amp;rsquo;re getting into the fun bits: Multi-agent orchestration!&lt;/p&gt;
&lt;p&gt;For more complex tasks, we can (or &lt;strong&gt;should&lt;/strong&gt;) use a multi-agent workflow. This involves having a separate commander model that orchestrates tasks to implementation models.&lt;/p&gt;
&lt;p&gt;The commander model creates a task list in a knowledge base such as Linear or the design doc, creates the stories, then delegates to the sub-agents for implementation.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./multiagent.png&#34; alt=&#34;multiagent&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Advantages of this are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;em&gt;commander&lt;/em&gt; model splits the work like a PM, defines acceptance criteria for each task, reviews code, updates tasks.&lt;/li&gt;
&lt;li&gt;The &lt;em&gt;implementation&lt;/em&gt; models are VERY accurate and specific since they only do one small task before returning.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No context pollution&lt;/strong&gt; - the sub-agents are always fresh in context.&lt;/li&gt;
&lt;li&gt;Tasks can be very long and will still be accurate because of this.&lt;/li&gt;
&lt;li&gt;Tasks &lt;strong&gt;can be resumed and stopped&lt;/strong&gt; at any time because we keep track through the task list or sub-issues.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We can use MCP with tools like Jira or Linear for orchestration, so the commander creates the subtasks and tells the agents to pull them from the issue tracker with MCP.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example how it works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Commander model processes a design doc or epic issue&lt;/li&gt;
&lt;li&gt;Commander model does basic research and splits the design doc or epic into subtasks (if not done yet), then optionally creates those in Linear, GitHub or somewhere else&lt;/li&gt;
&lt;li&gt;Commander model spawns sub-agents for each task in sequence&lt;/li&gt;
&lt;li&gt;Sub-agent implements spins up with &lt;em&gt;new&lt;/em&gt; context, clear instructions what to do and how to complete the task&lt;/li&gt;
&lt;li&gt;Commander model reviews the finished task, marks the issue as completed, and moves on to task #2&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./image-20250603161926079.png&#34; alt=&#34;image-20250603161926079&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;The issue tickets or markdown document serves as a boundary between commander and sub-agent to pass information across. The sub-agent knows it&amp;rsquo;s instructions, but has the ability to read in more information from the parent if required or if something is unclear.&lt;/p&gt;
&lt;p&gt;This workflow can be achieved in any agent by having 2 different prompts and feeding them in, but &lt;strong&gt;Roo&lt;/strong&gt; stands out to me for having all of this built-in with it&amp;rsquo;s multi-mode setup.&lt;/p&gt;
&lt;p&gt;I have built plugins that can do this for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vscode: &lt;a href=&#34;https://github.com/dvcrn/copilot-task-delegate&#34;&gt;https://github.com/dvcrn/copilot-task-delegate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Delegating with MCP to q/claude: &lt;a href=&#34;https://github.com/dvcrn/mcp-server-subagent&#34;&gt;https://github.com/dvcrn/mcp-server-subagent&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For completeness sake, here is one prompt I frequently use in &lt;em&gt;Roo&lt;/em&gt; which adds a todo checklist to the markdown design doc. I then have variations of this prompt that use Linear or GitHub issues as backend.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-markdown&#34; data-lang=&#34;markdown&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Your role is to coordinate complex workflows by delegating tasks to specialized modes. As an orchestrator, you should:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Break this down into subtasks that can be implemented as individual logical chunks. Don&amp;#39;t make them too small, but also not too big. Think of them as JIRA subtasks. 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;If a design doc is available, I want you to add all these subtasks to the design doc with a checklist on the current implementation progress.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;For each subtask, use the &lt;span style=&#34;color:#e6db74&#34;&gt;`new_task`&lt;/span&gt; tool to delegate. Choose the most appropriate mode for the subtask&amp;#39;s specific goal and provide comprehensive instructions in the &lt;span style=&#34;color:#e6db74&#34;&gt;`message`&lt;/span&gt; parameter. These instructions must include:  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;*&lt;/span&gt; All necessary context from the parent task or previous subtasks required to complete the work.  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;*&lt;/span&gt; A clearly defined scope, specifying exactly what the subtask should accomplish.  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;*&lt;/span&gt; An explicit statement that the subtask should &lt;span style=&#34;font-style:italic&#34;&gt;*only*&lt;/span&gt; perform the work outlined in these instructions and not deviate.  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;*&lt;/span&gt; If available, a reference to the design doc, files to change, scope
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;*&lt;/span&gt; An instruction for the subtask to signal completion by using the &lt;span style=&#34;color:#e6db74&#34;&gt;`attempt_completion`&lt;/span&gt; tool, providing a concise yet thorough summary of the outcome in the &lt;span style=&#34;color:#e6db74&#34;&gt;`result`&lt;/span&gt; parameter, keeping in mind that this summary will be the source of truth used to keep track of what was completed on this project.  
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;*&lt;/span&gt; A statement that these specific instructions supersede any conflicting general instructions the subtask&amp;#39;s mode might have.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;*&lt;/span&gt; Instructions to commit the changed files (only the changed files, NOT &lt;span style=&#34;color:#e6db74&#34;&gt;`git add .`&lt;/span&gt;) after completion, after all &lt;span style=&#34;color:#e6db74&#34;&gt;`build`&lt;/span&gt; and &lt;span style=&#34;color:#e6db74&#34;&gt;`format`&lt;/span&gt; instructions (such as &lt;span style=&#34;color:#e6db74&#34;&gt;`make build`&lt;/span&gt; or &lt;span style=&#34;color:#e6db74&#34;&gt;`make format`&lt;/span&gt; if available)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;For the git commit message, Start the message with what the commit does, a verb, first letter capitalized. Eg Update xxx to yyy, Change bbb to better ccc, and so on. When reading the log, we should be able to read it as &amp;#39;When this commit is applied, it will &amp;lt;followed by the commit message&amp;gt;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Once the subtasks returns, I want you to check the checkbox in the design doc to mark the task as completed.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;A subtask is only considered completed when a commit has been made. Make sure you define the acceptance criteria to the subtask.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;When the task returns, review the code for completeness and see if you spot any obvious issues.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Another benefit of this is that now, with asynchronous agents on GitHub or Google Jules becoming better, that we can delegate some of our subtasks directly to those for handling.&lt;/p&gt;
&lt;h3 id=&#34;summarizing-what-we-learned-so-far&#34;&gt;Summarizing what we learned so far&lt;/h3&gt;
&lt;p&gt;Agents are extremely powerful if guided correctly, but we&amp;rsquo;re not at a point yet where they can be left unsupervised.&lt;/p&gt;
&lt;p&gt;Always define clear boundaries, be that in simple tasks, behavior or complex projects.&lt;/p&gt;
&lt;p&gt;The more complex a task, the more you should probably split it and break it up into sub-stories that can be worked on independently to not run into context pollution issues: Keep it short and to the point for the best results.&lt;/p&gt;
&lt;p&gt;Create new sessions as often as you can, disable MCP you don&amp;rsquo;t need and use built-in &lt;code&gt;/compact&lt;/code&gt; commands to further reduce amount of tokens in the context window at any time.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m hoping to expand on this series further in the future when agents evolved a bit more. I&amp;rsquo;m especially excited about &lt;a href=&#34;https://jules.google.com&#34;&gt;Jules&lt;/a&gt; and &lt;a href=&#34;https://docs.github.com/en/copilot/building-copilot-extensions/building-a-copilot-agent-for-your-copilot-extension/about-copilot-agents&#34;&gt;GitHub Copilot Agents&lt;/a&gt; and started using them extensively for my own projects.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;I&amp;rsquo;ve created a &lt;a href=&#34;https://david.coffee/agentic-coding-with-4-personas/&#34;&gt;follow-up post&lt;/a&gt; for 2026 with a multi agent persona system&lt;/em&gt;&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/vibe-coding-advanced/vibebot.jpeg" /></p><p>This post is a direct continuation of <a href="https://david.coffee/vibe-coding-essentials/">Better Vibe Coding - Part 1, Foundation &amp; Basics</a>. If you haven’t read that yet, start there for the basics.</p>
<hr>
<h2 id="2-vibe-coding-complex-tasks">2. &ldquo;Vibe Coding&rdquo; complex tasks</h2>
<p>Phew, now that we got the basics covered, let’s go into more advanced territory.</p>
<p>If you vibe coded in bigger codebases you’ll likely face issues with the agent&rsquo;s quality going down the drain the more files it reads, and the more complex a task becomes.</p>
<p>When an agent reads in more tokens, eventually it’ll flush out stuff that came in the beginning of the conversation and lose context. We simply can’t afford to read files that we don’t need to read, and features that span multiple files need different handling.</p>
<p>All of these things I mention here is what I use for my projects on a daily basis and consider crucial.</p>
<h3 id="21-split-plan-phases-from-implementation-phases">2.1 Split plan phases from implementation phases</h3>
<p>Putting it as it is: Purely vibe coding complex features (as in, opening a prompt and telling an agent to &ldquo;implement this feature&rdquo;) is just not doable currently due to limitations mentioned previously.</p>
<p>Imagine treating a new-joiner in your team like an AI agent. Would you just tell them to &ldquo;implement this feature&rdquo; without any guidance? Yeah very likely not (unless you want to scare your new hire away already).</p>
<p>So how can we work around it? By thinking like a manager: Separating planning from implementation phases.</p>
<p>Part of implementing any feature in a professional context always involves a planning phase before we can create use stories or a design document, so why should working with agents be any different?</p>
<p><strong>Planning phase</strong></p>
<ul>
<li>Understand and clarify the ask: What do we need to do, why</li>
<li>Define the acceptance criteria</li>
<li>Read a lot of code</li>
<li>Research</li>
<li>Come up with a plan</li>
<li>Output a design document</li>
</ul>
<p><strong>Implementation phase</strong></p>
<ul>
<li>Often a <strong>new</strong> session to flush the context</li>
<li>Reads the plan and implements it</li>
</ul>
<p>For the planning phase, I use a model with a bigger context since it has to gobble up a lot of code (gemini-2.5-pro or GPT4.1). This is where <em>thinking-models</em> really shine and should be used: Devise a good plan, doesn’t matter how long it takes.</p>
<p>For the implementation phase, thinking is usually not required because the heavy lifting has already been done in the planning phase.</p>
<p>
<img class="img-fluid" src="./flow.png" alt="approach diagram" />

</p>
<p><a href="https://docs.roocode.com">Roo</a> or <a href="https://cline.bot">Cline</a> have <em>PLAN</em> phases built-in, but we can easily emulate that in other tools as well with a prompt like the following (I have this as a custom Cursor mode):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span>You are currently in PLAN mode.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> PLAN mode is for creating detailed plans and strategies, not for implementing code changes
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Your goal is to gather information, conduct research, and develop a comprehensive plan before any implementation
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Focus on understanding requirements and architecting solutions rather than writing actual code
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Use tools like <span style="color:#e6db74">`read_file`</span> and <span style="color:#e6db74">`search_files`</span> to gather context about the existing codebase
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Ask clarifying questions to ensure you fully understand what&#39;s needed
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Create detailed, well-researched plans with clear steps and reasoning
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Use Mermaid diagrams when helpful to visualize architecture, workflows, or processes
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> You may include small code snippets to illustrate concepts, but avoid suggesting full implementations
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Engage in back-and-forth discussion to refine the plan until it meets requirements
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> When a satisfactory plan is established, suggest switching to ACT mode for implementation
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Remember: in PLAN mode, your focus is exclusively on planning, not doing
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>PREFIX ANY MESSAGE YOU WRITE WITH &#34;PLAN MODE:&#34; 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>THIS IS VERY VERY IMPORTANT!!!
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>If you get an error of edit_file not being available, then THIS IS A REMINDER TO YOU THAT WE ARE IN PLAN MODE!!!! REMEMBER YOUR INSTRUCTIONS
</span></span></code></pre></div><p>Once the plan is generated, I pass it to a new agent to do the actual implementation.</p>
<h3 id="22-resumption-and-hand-off-with-design-docs-and-logs">2.2 Resumption and hand-off with design docs and logs</h3>
<p>Smaller edits can be made one-shot by passing the plan to an implementor model and letting it do its thing, but bigger plans may still exceed the context window and drop in quality.</p>
<p>For this reason, I always use 2 document formats: A <strong>clearly defined design doc</strong> and a <strong>log</strong>.</p>
<p>The design doc is the output of the planning phase. Its purpose is similar to the “onboarding document” we talked about previously, but with the objective to onboard someone onto a specific feature, so the same rules apply: Include everything that someone with no knowledge should know about this feature:</p>
<ul>
<li>What needs to be done and why</li>
<li>Link to documents, tickets</li>
<li>Technical design</li>
<li>Clearly defined list with proposed high-level changes
<ul>
<li>What files need to be looked at, where to find them</li>
<li>What changes do we need to do in those files</li>
</ul>
</li>
</ul>
<p>The purpose of this document is to condense the huge context window research that the planning phase did into something that is actionable, and more important: <em>reviewable (by us)</em>. <strong>This is the final big step before implementation, so we need to make sure that the idea the LLM has aligns with what we want to do.</strong> I iterate on these design docs a couple times until it&rsquo;s in a shape that I&rsquo;m happy with.</p>
<p><strong>Always</strong> define a guide or template for these documents. Don’t let the AI do what it wants, we want the documents to be consistent and contain the information we already know we need for effective implementation.</p>
<p>A good format (from my experience) is following similar principles to the rules and manual document. This document is not intended for humans, it&rsquo;s for LLMs, so it needs to be able to quickly bring the agent up to speed and point it towards the files and important bits that it needs to know, while providing context to do additional research if needed.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span># Agent UI Simplification Design Doc
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="font-weight:bold">**Date:**</span> 2025-04-24
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="font-weight:bold">**Author:**</span> GitHub Copilot
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="font-weight:bold">**Linear Issue:**</span> MIC-28
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## 1. Overview
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>Simplify the Agent interface by merging the configuration options (currently under a separate &#34;Settings&#34; tab) into the main &#34;Execute&#34; tab. Rename the combined tab to &#34;Agent&#34;. The &#34;Execution History&#34; tab will remain separate.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## 2. Motivation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>The current tab structure (Execute, Settings, Execution History) with nested tabs (Configuration, Functions) within Settings is convoluted. Users need to switch tabs frequently to configure and run an agent. Combining configuration and execution into a single view streamlines the workflow.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## 3. Proposed Changes
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span>   <span style="font-weight:bold">**Modify `app/agents/[id]/page.tsx`**</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">-</span>   Remove the &#34;Settings&#34; <span style="color:#e6db74">`TabsTrigger`</span> and <span style="color:#e6db74">`TabsContent`</span>.
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">-</span>   Rename the &#34;Execute&#34; <span style="color:#e6db74">`TabsTrigger`</span> to &#34;Agent&#34;.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span>   <span style="font-weight:bold">**Modify `components/agents/agent-execution-panel.tsx`**</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">-</span>   Integrate the UI elements and logic currently present in <span style="color:#e6db74">`components/agents/agent-config-panel.tsx`</span> (Agent Name, Description, Model, System Prompt, Function Linking).
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">-</span>   Arrange the configuration elements logically within the panel, likely above or alongside the execution controls.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span>   <span style="font-weight:bold">**Deprecate/Remove `components/agents/agent-config-panel.tsx`**</span>: This component will no longer be needed as its functionality is merged into the execution panel.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## 4. Technical Design
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">1.</span>  <span style="font-weight:bold">**Update Tabs in `AgentPage`**</span>: Modify the <span style="color:#e6db74">`Tabs`</span>, <span style="color:#e6db74">`TabsList`</span>, and <span style="color:#e6db74">`TabsContent`</span> structure in <span style="color:#e6db74">`app/agents/[id]/page.tsx`</span> to reflect the two-tab layout (&#34;Agent&#34;, &#34;Execution History&#34;).
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">2.</span>  <span style="font-weight:bold">**Merge Components**</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">-</span>   Identify the specific UI sections and state management logic within <span style="color:#e6db74">`AgentConfigPanel`</span> (e.g., form handling for name/description, model selection, system prompt editor, function linking UI).
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">-</span>   Transfer these UI sections and their associated logic (props, state, handlers) into <span style="color:#e6db74">`AgentExecutionPanel`</span>. Ensure props (<span style="color:#e6db74">`agent`</span>, <span style="color:#e6db74">`functions`</span>) are correctly utilized.
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">-</span>   Adjust the layout within <span style="color:#e6db74">`AgentExecutionPanel`</span> to accommodate the new configuration elements. A vertical stack (Config section above Execution section) seems simplest initially.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">3.</span>  <span style="font-weight:bold">**Refactor/Remove `AgentConfigPanel`**</span>: Once all functionality is migrated, <span style="color:#e6db74">`AgentConfigPanel`</span> can be removed or refactored if any parts are still reusable independently (unlikely).
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">4.</span>  <span style="font-weight:bold">**Testing**</span>: (Future step) Add/update tests for <span style="color:#e6db74">`AgentExecutionPanel`</span> to cover the integrated configuration options.
</span></span></code></pre></div><p>The counterpart of the design doc is the work log - a document the agent needs to create after it is done. This document references the design doc and explains what the agent actually did.</p>
<p>The purpose of this document is to glance at what has been done, but also to potentially feed into a new agent to get up to speed on this feature, to do additional changes.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span># Handoff: Agent UI Gamification - 2025-04-24
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="font-weight:bold">**Author:**</span> GitHub Copilot
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## TL;DR
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>We&#39;re redesigning the Agent configuration screen (<span style="color:#e6db74">`app/agents/[id]/page.tsx`</span>) to be more visual and intuitive, replacing the old text-heavy forms with a &#34;gamified&#34; interface centered around a robot avatar.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="font-weight:bold">**Design Doc:**</span> <span style="color:#e6db74">`docs/designdocs/agent-gamified-ui.md`</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## What We Did So Far
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">1.</span>  <span style="font-weight:bold">**Created `AgentVisualConfigurator` Component:**</span> Built a new component (<span style="color:#e6db74">`components/agents/agent-visual-configurator.tsx`</span>) that displays:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">-</span> A robot avatar (<span style="color:#e6db74">`BotIcon`</span>).
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">-</span> The agent&#39;s objective/system prompt (currently display-only).
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">-</span> Visual &#34;Skill Slots&#34; for linked functions.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">2.</span>  <span style="font-weight:bold">**Integrated into Agent Page:**</span> Replaced the old configuration cards in <span style="color:#e6db74">`components/agents/agent-execution-panel.tsx`</span> with the new <span style="color:#e6db74">`AgentVisualConfigurator`</span>.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">3.</span>  <span style="font-weight:bold">**Skill Slot Functionality:**</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">-</span> **Display:** Filled slots show the function name (with <span style="color:#e6db74">`SwordIcon`</span> 🗡️) and workspace.
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">-</span> **Add:** Clicking an empty slot (<span style="color:#e6db74">`+`</span>) opens the <span style="color:#e6db74">`FunctionSearch`</span> dialog to find and link a new function.
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">-</span> **Edit:** Clicking a <span style="font-style:italic">_filled_</span> slot opens the <span style="color:#e6db74">`FunctionLinkForm`</span> modal, allowing the user to edit the description (when the agent should use this function).
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">-</span> **Remove:** Clicking the trash icon (<span style="color:#e6db74">`Trash2Icon`</span>) on a filled slot removes the function link.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">4.</span>  <span style="font-weight:bold">**Data Fetching:**</span> Updated React Query mutation hooks (<span style="color:#e6db74">`lib/hooks/use-agent-functions.ts`</span>) to <span style="color:#e6db74">`await`</span> query invalidation on success, ensuring the UI reflects changes immediately after adding/editing/removing functions without needing a page refresh.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">5.</span>  <span style="font-weight:bold">**Styling:**</span> Applied basic Tailwind CSS and shadcn UI styling to the new component and slots.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Current State
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> The visual layout is in place on the agent page.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Users can view the agent&#39;s objective.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Users can add, edit the description of, and remove function links via the skill slots and associated modals (<span style="color:#e6db74">`FunctionSearch`</span>, <span style="color:#e6db74">`FunctionLinkForm`</span>).
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> The underlying data updates correctly and the UI refreshes thanks to query invalidation.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Next Steps / What&#39;s Left
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">1.</span>  <span style="font-weight:bold">**Save Objective:**</span> Hook up the objective/system prompt <span style="color:#e6db74">`textarea`</span> in <span style="color:#e6db74">`AgentVisualConfigurator`</span> to actually save changes (likely using <span style="color:#e6db74">`useUpdateAgent`</span>).
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">2.</span>  <span style="font-weight:bold">**Run Agent Integration:**</span> Connect the &#34;Try Your Agent&#34; input and button (<span style="color:#e6db74">`AgentExecutionPanel`</span>) to work with this new view (it might already work, but needs verification).
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">3.</span>  <span style="font-weight:bold">**Execution History:**</span> Implement the display area for execution logs below the configurator.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">4.</span>  <span style="font-weight:bold">**Testing:**</span> Write tests for <span style="color:#e6db74">`AgentVisualConfigurator`</span> and potentially update tests for <span style="color:#e6db74">`AgentExecutionPanel`</span>.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">5.</span>  <span style="font-weight:bold">**Refinement (Optional):**</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">-</span> Add subtle animations (e.g., robot idle, feedback on add/remove).
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">-</span> Further styling improvements.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">6.</span>  <span style="font-weight:bold">**Final Build:**</span> Run <span style="color:#e6db74">`bun run build`</span> to ensure everything builds correctly.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">7.</span>  <span style="font-weight:bold">**Documentation:**</span> Update any relevant user-facing docs if needed.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>This should get the next person up to speed! Lmk if you need anything else. 🔥
</span></span></code></pre></div><h3 id="23-multi-agent-workflow-with-agent-to-agent-delegation">2.3 Multi-agent workflow with agent-to-agent delegation</h3>
<p>Now we&rsquo;re getting into the fun bits: Multi-agent orchestration!</p>
<p>For more complex tasks, we can (or <strong>should</strong>) use a multi-agent workflow. This involves having a separate commander model that orchestrates tasks to implementation models.</p>
<p>The commander model creates a task list in a knowledge base such as Linear or the design doc, creates the stories, then delegates to the sub-agents for implementation.</p>
<p>
<img class="img-fluid" src="./multiagent.png" alt="multiagent" />

</p>
<p>Advantages of this are:</p>
<ul>
<li>The <em>commander</em> model splits the work like a PM, defines acceptance criteria for each task, reviews code, updates tasks.</li>
<li>The <em>implementation</em> models are VERY accurate and specific since they only do one small task before returning.</li>
<li><strong>No context pollution</strong> - the sub-agents are always fresh in context.</li>
<li>Tasks can be very long and will still be accurate because of this.</li>
<li>Tasks <strong>can be resumed and stopped</strong> at any time because we keep track through the task list or sub-issues.</li>
</ul>
<p>We can use MCP with tools like Jira or Linear for orchestration, so the commander creates the subtasks and tells the agents to pull them from the issue tracker with MCP.</p>
<p>Here&rsquo;s an example how it works:</p>
<ol>
<li>Commander model processes a design doc or epic issue</li>
<li>Commander model does basic research and splits the design doc or epic into subtasks (if not done yet), then optionally creates those in Linear, GitHub or somewhere else</li>
<li>Commander model spawns sub-agents for each task in sequence</li>
<li>Sub-agent implements spins up with <em>new</em> context, clear instructions what to do and how to complete the task</li>
<li>Commander model reviews the finished task, marks the issue as completed, and moves on to task #2</li>
</ol>
<p>
<img class="img-fluid" src="./image-20250603161926079.png" alt="image-20250603161926079" />

</p>
<p>The issue tickets or markdown document serves as a boundary between commander and sub-agent to pass information across. The sub-agent knows it&rsquo;s instructions, but has the ability to read in more information from the parent if required or if something is unclear.</p>
<p>This workflow can be achieved in any agent by having 2 different prompts and feeding them in, but <strong>Roo</strong> stands out to me for having all of this built-in with it&rsquo;s multi-mode setup.</p>
<p>I have built plugins that can do this for:</p>
<ul>
<li>Vscode: <a href="https://github.com/dvcrn/copilot-task-delegate">https://github.com/dvcrn/copilot-task-delegate</a></li>
<li>Delegating with MCP to q/claude: <a href="https://github.com/dvcrn/mcp-server-subagent">https://github.com/dvcrn/mcp-server-subagent</a></li>
</ul>
<p>For completeness sake, here is one prompt I frequently use in <em>Roo</em> which adds a todo checklist to the markdown design doc. I then have variations of this prompt that use Linear or GitHub issues as backend.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span>Your role is to coordinate complex workflows by delegating tasks to specialized modes. As an orchestrator, you should:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Break this down into subtasks that can be implemented as individual logical chunks. Don&#39;t make them too small, but also not too big. Think of them as JIRA subtasks. 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>If a design doc is available, I want you to add all these subtasks to the design doc with a checklist on the current implementation progress.
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>For each subtask, use the <span style="color:#e6db74">`new_task`</span> tool to delegate. Choose the most appropriate mode for the subtask&#39;s specific goal and provide comprehensive instructions in the <span style="color:#e6db74">`message`</span> parameter. These instructions must include:  
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> All necessary context from the parent task or previous subtasks required to complete the work.  
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> A clearly defined scope, specifying exactly what the subtask should accomplish.  
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> An explicit statement that the subtask should <span style="font-style:italic">*only*</span> perform the work outlined in these instructions and not deviate.  
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> If available, a reference to the design doc, files to change, scope
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> An instruction for the subtask to signal completion by using the <span style="color:#e6db74">`attempt_completion`</span> tool, providing a concise yet thorough summary of the outcome in the <span style="color:#e6db74">`result`</span> parameter, keeping in mind that this summary will be the source of truth used to keep track of what was completed on this project.  
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> A statement that these specific instructions supersede any conflicting general instructions the subtask&#39;s mode might have.
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">*</span> Instructions to commit the changed files (only the changed files, NOT <span style="color:#e6db74">`git add .`</span>) after completion, after all <span style="color:#e6db74">`build`</span> and <span style="color:#e6db74">`format`</span> instructions (such as <span style="color:#e6db74">`make build`</span> or <span style="color:#e6db74">`make format`</span> if available)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>For the git commit message, Start the message with what the commit does, a verb, first letter capitalized. Eg Update xxx to yyy, Change bbb to better ccc, and so on. When reading the log, we should be able to read it as &#39;When this commit is applied, it will &lt;followed by the commit message&gt;&#39;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Once the subtasks returns, I want you to check the checkbox in the design doc to mark the task as completed.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>A subtask is only considered completed when a commit has been made. Make sure you define the acceptance criteria to the subtask.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>When the task returns, review the code for completeness and see if you spot any obvious issues.
</span></span></code></pre></div><p>Another benefit of this is that now, with asynchronous agents on GitHub or Google Jules becoming better, that we can delegate some of our subtasks directly to those for handling.</p>
<h3 id="summarizing-what-we-learned-so-far">Summarizing what we learned so far</h3>
<p>Agents are extremely powerful if guided correctly, but we&rsquo;re not at a point yet where they can be left unsupervised.</p>
<p>Always define clear boundaries, be that in simple tasks, behavior or complex projects.</p>
<p>The more complex a task, the more you should probably split it and break it up into sub-stories that can be worked on independently to not run into context pollution issues: Keep it short and to the point for the best results.</p>
<p>Create new sessions as often as you can, disable MCP you don&rsquo;t need and use built-in <code>/compact</code> commands to further reduce amount of tokens in the context window at any time.</p>
<p>I&rsquo;m hoping to expand on this series further in the future when agents evolved a bit more. I&rsquo;m especially excited about <a href="https://jules.google.com">Jules</a> and <a href="https://docs.github.com/en/copilot/building-copilot-extensions/building-a-copilot-agent-for-your-copilot-extension/about-copilot-agents">GitHub Copilot Agents</a> and started using them extensively for my own projects.</p>
<p><em>I&rsquo;ve created a <a href="https://david.coffee/agentic-coding-with-4-personas/">follow-up post</a> for 2026 with a multi agent persona system</em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Better Vibe Coding - Part 1, Foundation &amp; Basics</title>
      <link>https://david.coffee/vibe-coding-part-1/</link>
      <pubDate>Wed, 07 May 2025 10:00:00 +0900</pubDate>
      
      <guid>https://david.coffee/vibe-coding-part-1/</guid><media:content url="https://david.coffee/vibe-coding-part-1/vibe-coding-beach.jpeg" medium="image" />
          <enclosure url="https://david.coffee/vibe-coding-part-1/vibe-coding-beach.jpeg" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/vibe-coding-part-1/vibe-coding-beach.jpeg&#34; /&gt;&lt;/p&gt;&lt;p&gt;Today let’s talk about “Vibe Coding”, the current trend of writing code by chatting with autonomous AI agents.&lt;/p&gt;
&lt;p&gt;I see a lot happening around Vibe Coding, from people getting hacked because their site falls apart, to people bootstrapping entire apps in hours instead of weeks.&lt;/p&gt;
&lt;p&gt;I’ve been big into vibe-coding myself and wanted to share some tips and tricks on how to get the most value out of your AI agents.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Who am I and why should you even listen to me?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I’ve been working in tech for the past 14 or so years. 10+ in various roles from Engineer, TL, architect, and 3~4+ years as Engineering Manager in charge of teams of ~10 engineers. I’ve built sensitive high-availability systems used by millions of people, handling all kinds of sensitive data such as financial and banking information.&lt;/p&gt;
&lt;p&gt;I want to believe that I&amp;rsquo;m fairly good with coding and designing complex systems, and those skills just happen to apply quite nicely to telling AI agents what to do as well.&lt;/p&gt;
&lt;h2 id=&#34;wtf-is-vibe-coding&#34;&gt;Wtf is “Vibe Coding”?&lt;/h2&gt;
&lt;p&gt;Let’s clear this up first - Vibe Coding roughly translates to: Building apps/websites/features by chatting with an AI agent instead of writing code yourself.&lt;/p&gt;
&lt;p&gt;Vibe Coding can be asking an AI agent to do simple edits like “Add a new function that does X to this file”, or complex multi-file features in the lines of “Add a new button to this page that does X”.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./image-20250507234139653.png&#34; alt=&#34;Vibe Coding with a Gen Alpha AI&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;In a very rough sense, Vibe Coding is similar to asking an engineer to do something and assigning them a task: The engineer has to figure out what to do, where the files are and how to accomplish the task.&lt;/p&gt;
&lt;h3 id=&#34;the-problem-with-vibe-coding-context&#34;&gt;The problem with Vibe Coding: Context&lt;/h3&gt;
&lt;p&gt;Because of how agents work, each task is effectively similar to a new engineer that just joined your team: You have to teach them everything they need to know about your project to onboard them.&lt;/p&gt;
&lt;p&gt;Sure, the engineer, if smart enough, can still be productive from day 1, but the likelihood of them doing something they shouldn’t, or re-implementing something that already exists is high. Combine that with the superhuman speed of spitting out code in seconds and you have a recipe for disaster if not supervised: Broken conventions, re-implemented functions, security holes.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./GpWULVRboAACSTA.jpeg&#34; alt=&#34;Vibe Coding going wrong&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;And the bad thing: Your agent won’t learn by itself. Once ‘onboarded’ it can be productive for the task you’ve given it, but once you create a new session, we have to start from scratch.&lt;/p&gt;
&lt;p&gt;In an optimal world, we could just give the agent the entire codebase as context, but with current limitations, that won’t be possible anytime soon. The opposite is true: The more files we add to an agent&amp;rsquo;s context, the more chaotic and unfocused it will get. Not good.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Everything&lt;/em&gt; competes for context. Even before we write a single word, the context has already been pre-filled with instructions by your editor, available tools and how to use those tools, MCP, etc.&lt;/p&gt;
&lt;p&gt;To give you an idea, the system prompts of different editors (remember when the max context window we had was under 4000 tokens?):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cursor: 3900 words&lt;/li&gt;
&lt;li&gt;Cline: 2990 words&lt;/li&gt;
&lt;li&gt;Copilot: 1400 words&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So what we have to do: &lt;strong&gt;Onboard an agent as efficiently as possible&lt;/strong&gt; - the least amount of tokens that covers &lt;em&gt;everything&lt;/em&gt; the agent needs to know.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll structure this post into 2 parts: 1 is talking about the basics of getting more quality out of Vibe Coding, part 2 will be going into more advanced task orchestration.&lt;/p&gt;
&lt;h2 id=&#34;1-the-basics-of-stronger-agents&#34;&gt;1. The basics of stronger agents&lt;/h2&gt;
&lt;p&gt;In 1. We’ll talk about how to guide agents and bring them up to speed on your code&lt;/p&gt;
&lt;h3 id=&#34;11-a-strong-entry-point&#34;&gt;1.1 A strong entry point&lt;/h3&gt;
&lt;p&gt;The first thing I do in every project I work on is to generate a strong entry point for the agent. Think of it as a document that has a condensed, high-information density overview of what the project is about. Previously I called this “LLM.md”, but nowadays it&amp;rsquo;s standardized to &lt;code&gt;AGENTS.md&lt;/code&gt; for Codex and lots of editors, &lt;code&gt;CLAUDE.md&lt;/code&gt; for Claude Code, or &lt;code&gt;GEMINI.md&lt;/code&gt; for Gemini. I usually pick one and symlink all of them to the same file so every tool finds the right context immediately.&lt;/p&gt;
&lt;p&gt;I build each part of this document with the question: If I give this document to someone who has never seen the code in here, will they have enough information to research the rest themselves?&lt;/p&gt;
&lt;p&gt;Everything not in this document will require the agent to research it on their own, increasing the likelihood of messing something up, missing something altogether or misunderstanding.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;- What is the project
  - 1-2 lines about what the purpose and goal of the project is
- Key architecture
  - “Uses clean architecture”
  - “Uses MVVN”
  - “All data fetching logic is done with react-query”
  - “Uses bun”, “NodeJS 18+”
  - “Uses jest for tests”
- Key commands to interact with the repository
  - Build command: How to build the app “bun build”, “make build”
  - Lint: How to check the code for errors “make lint”, “bun run lint”
  - Test: How to run tests “bun test”, “make test”, etc
- What are the key parts and where to find them 
  - “/app” - NextJS app directory with routing and pages
  - “/app/components” - React components, includes UI
  - “/repository” - Database layer, contains functions for fetching data
  - “/services” - Directory for connectors to external services and APIs
- Very simple guides on key questions, with more documents going in detail
  - “How to write tests”
    - “All tests go into /test and have to use jest as test runner
    - “Always mock with X. All database interaction has to be mocked”
    - “For react-query testing, read xxx.md”
    - “For React component testing, read yyy.md”
  - “How to create a new API route”
    - “App routes always go into /app/routes and have to use Hono”
    - “Routes are to be imported and mounted into the router at /app/routes/routes.tsx”
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The point of this document is not to be the knowledge dump for everything going on, but more a booster: It gives a quick refresher of what the project is about, where to find what, what the stack and architecture is, and more importantly: Where to continue checking if something is missing.&lt;/p&gt;
&lt;p&gt;Thanks to this, an agent doesn&amp;rsquo;t have to re-scan the entire repository and &lt;code&gt;cd+ls&lt;/code&gt; a dozen times, but can focus on targeted scans in the directories that it needs to depending on the task.&lt;/p&gt;
&lt;h3 id=&#34;12-a-strong-ruleset&#34;&gt;1.2 A strong ruleset&lt;/h3&gt;
&lt;p&gt;On top of the starting point, I have a separate rule file that is very explicit about how this repository should get handled. This usually goes into whatever agent’s rule definition I currently use. For Cursor, that’s &lt;code&gt;.cursor/rules&lt;/code&gt;, for Cline that’s &lt;code&gt;.clinerules&lt;/code&gt;, for Roo that’s &lt;code&gt;.roo/rules&lt;/code&gt;, for Zed that’s &lt;code&gt;.zed/&lt;/code&gt; and so on.&lt;/p&gt;
&lt;p&gt;A great feature of modern editors is that they allow you to specify &lt;em&gt;when&lt;/em&gt; these rules should get imported. For example, in Cursor, you can specify that a rule only applies to &lt;code&gt;*.js&lt;/code&gt; files or &lt;code&gt;*.md&lt;/code&gt; files. This is a massive context-saver: The agent doesn&amp;rsquo;t need to know about your database conventions when it&amp;rsquo;s just updating a README.&lt;/p&gt;
&lt;p&gt;For general rules that apply to the entire repository regardless of file type, I often skip the editor-specific files and put them directly into my entry point files (&lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;GEMINI.md&lt;/code&gt;, or &lt;code&gt;CLAUDE.md&lt;/code&gt;). This ensures that no matter which tool I&amp;rsquo;m using, the core guardrails are always present.&lt;/p&gt;
&lt;p&gt;While the starting point is for &lt;em&gt;getting the agent onboarded&lt;/em&gt;, the rules are for defining constraints the agent must adhere to. So:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Code conventions&lt;/li&gt;
&lt;li&gt;Test conventions&lt;/li&gt;
&lt;li&gt;Architecture decisions&lt;/li&gt;
&lt;li&gt;Git &amp;amp; branching rules&lt;/li&gt;
&lt;li&gt;Acceptance criteria for all tasks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I keep the rule file unformatted, numbered. I can write rules in markdown but often combine this with the starting point document, so we’re losing precious context for each token.&lt;/p&gt;
&lt;p&gt;Here’s an example from a recent project:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;1. Follow TDD: Write a test first, make sure the test fails, then implement the feature. See guides: [Component Testing Guide](mdc:docs/component-testing.md) and [React Query Testing](mdc:docs/react-query-testing.md). Place component tests in test/components/*.test.tsx.
2. Always use `bun` as the package manager and build tool. Run tests with `bun test` and format code with `bun format`. Never use other testing frameworks (like vitest). This project uses bun for testing.
3. Always make sure the build passes before committing code.
4. KISS (Keep It Simple, Stupid): Favor the simplest solution that meets the requirements.
5. DRY (Don&amp;#39;t Repeat Yourself): Avoid code duplication. Extract reusable logic into functions or classes.
6. Docstrings: Write clear and concise docstrings for all classes, functions, and methods, explaining their purpose, parameters, and return values.
7. Create a new branch when working on a bigger feature. Small edits don&amp;#39;t need a new branch.
8. Commit messages start with a verb (first letter capitalized). Include a Linear ticket reference (MIC-XXX format) when available. Be descriptive about what changed and why.
9. Always create a design doc first when creating a new feature. Small edits don&amp;#39;t need this. Read docs/guides/design-doc-guide.md on how to write a design doc.
10. Always break down a design doc into small logical tasks that can be given to an engineer. Think stories. Add this to the design doc as a checklist and check them off as you finish work. Commit into your branch after each task is completed.
11. Use path aliases (`@/*`) instead of relative paths.
12. Follow the exact directory structure and import patterns from existing code
13. Run `bun format` after finishing all changes, and `bun run build` after a feature has been completed
14. Always write a log entry in logs/ with what you did. Name it YYYY-MM-DD-feature-name.md.
15. **All database operations must be implemented exclusively in `/lib/microfn` modules.**. **API routes (`/app/api`) and React components must never contain direct Prisma calls or any database logic.**. **API routes must call exported functions from `/lib/microfn` to perform any database operations.**.
16. Always run the vscode task &amp;#34;format&amp;#34;, or if you don&amp;#39;t have access to it, run `bun format` after each task is completed.
17. DO NOT fix lint errors about formatting, spacing, import order manually. Always run a `bun format` first as that may fix most issues. `bun format` will fix: Import order, removing unused imports, spacing, whitespace, punctuation.
18. Use shadcn and tailwind. Always look at other files in the same directory to understand style and structure.
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;13-use-scripts-for-an-automatic-feedback-loop-to-the-agent&#34;&gt;1.3 Use scripts for an automatic feedback loop to the agent&lt;/h3&gt;
&lt;p&gt;I consider this an absolute essential for Vibe Coding + AI automation, and maybe even more important than the rules we just talked about.&lt;/p&gt;
&lt;p&gt;Agents need to have a way to understand when the build is broken, the tests fail or the code is wrong. Linting can do that automatically if your editor+agent combo supports it, but even better to have it editor agnostic.&lt;/p&gt;
&lt;p&gt;These are simple scripts and tools I sprinkled into the previous 2 sections already: &lt;code&gt;make build&lt;/code&gt;, &lt;code&gt;bun build&lt;/code&gt;, &lt;code&gt;bun lint&lt;/code&gt;, &lt;code&gt;make test&lt;/code&gt;, &lt;code&gt;bun test&lt;/code&gt; - you name it.&lt;/p&gt;
&lt;p&gt;In my rule I always have something like this:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;14. Run `bun format` after finishing all changes, and `bun run build` after a feature has been completed
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;No matter what, the agent &lt;strong&gt;has&lt;/strong&gt; to run the build script after each task, which will immediately report back if something is broken. This feeds back into the agent that something went wrong, and prompts it to fix the problems.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In Swift, I do a &lt;code&gt;xcodebuild&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;In Golang, I do a &lt;code&gt;go build&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;In NextJS, I do a &lt;code&gt;next build&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Similarly, I always have a strong formatter active in my projects that reformats &lt;em&gt;everything&lt;/em&gt; and enforces coding style. Agents tend to do what they want when it comes to coding style, and instead of adding more and more rules to instruct them about small things (and taking up precious context space), it’s much easier to just re-format everything through &lt;code&gt;prettier&lt;/code&gt;, &lt;code&gt;xcformat&lt;/code&gt;, &lt;code&gt;mix format&lt;/code&gt;, and add that to the acceptance criteria of every task.&lt;/p&gt;
&lt;p&gt;If the agent runs &lt;code&gt;bun format &amp;amp;&amp;amp; bun build&lt;/code&gt; after each task, I can be sure that the code is formatted correctly and the build runs before a task is considered complete.&lt;/p&gt;
&lt;h3 id=&#34;14-improving-agents-through-self-improvement-and-reflection&#34;&gt;1.4 Improving agents through self-improvement and reflection&lt;/h3&gt;
&lt;p&gt;Another good thing to do, to make sure entry point and rules are updated, is to let the agents do it themselves.&lt;/p&gt;
&lt;p&gt;I often include an additional rule like the following. Depending on context size I either have this as a separate prompt somewhere that I can reference (aka “read @prompts/self-improve.md”), or part of my ruleset.&lt;/p&gt;
&lt;p&gt;If during a session the agent breaks convention and does something I don’t want it to do, I correct it and teach it the correct way. The purpose of the reflection is to review the conversation history up to this point and put the learnings back into the documents we have.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;# Self-Improving Cursor Rules Reflection

**Objective:** Offer opportunities to continuously improve `.cursor/rules` or [CLAUDE.md](mdc:CLAUDE.md) based on user interactions and feedback.

**Trigger:** Before completing any task that involved user feedback provided at any point during the conversation, or involved multiple non-trivial steps (e.g., multiple file edits, complex logic generation).

**Process:**

1.  **Offer Reflection:** Ask the user: &amp;#34;Before I complete the task, would you like me to reflect on our interaction and suggest potential improvements to the active `.cursor/rules` or `CLAUDE.md`?&amp;#34;
2.  **Await User Confirmation:** Proceed to completion immediately if the user declines or doesn&amp;#39;t respond affirmatively.
3.  **If User Confirms:**
    a.  **Review Interaction:** Synthesize all feedback provided by the user throughout the entire conversation history for the task. Analyze how this feedback relates to the active rules in `.cursor/rules` or [CLAUDE.md](mdc:CLAUDE.md) and identify areas where modified instructions could have improved the outcome or better aligned with user preferences.
    b.  **Identify Active Rules:** List the specific global and workspace `.cursor/rules` (or CLAUDE.md) files active during the task.
    c.  **Formulate &amp;amp; Propose Improvements:** Generate specific, actionable suggestions for improving the *content* of the relevant active rule files. Prioritize suggestions directly addressing user feedback. Output a changeset that is usable with `edit_file` to write these suggestions into `.cursor/rules` or `CLAUDE.md`, for the user to review. Don&amp;#39;t execute the tool yet..
    d.  **Await User Action on Suggestions:** Ask the user if they agree with the proposed improvements and if they&amp;#39;d like me to apply them *now* using the appropriate tool (`edit_file`). Apply changes if approved, then proceed to complete the task.

**Constraint:** Do not offer reflection if:
*   No `.cursor/rules` or `CLAUDE.md` were active.
*   The task was very simple and involved no feedback.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A prompt like this will then cause the agent to offer reflection after an edit session, if any feedback has been given, neat!&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./image-20250507235647830.png&#34; alt=&#34;Reflection in action&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;It’s of course important to not blindly accept all improvements. LLMs just generate text and aren’t properly smart (yet), so if you tell them to improve your rules, they will almost always suggest stuff to add to it even if not needed. Make sure you approach the rule file with care and keep it clean.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Follow these tips and you should have a strong foundation for more complex code changes that are consistent with your existing (and evolving) codebase. Especially when working together with other people, across models and tools, agents need to be aligned on the same guardrails to not go completely nuts.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;In &lt;a href=&#34;https://david.coffee/vibe-coding-advanced/&#34;&gt;the next post&lt;/a&gt;, we’ll dive into more advanced Vibe Coding: how to handle complex tasks, planning, hand-off, and multi-agent workflows.&lt;/em&gt;&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/vibe-coding-part-1/vibe-coding-beach.jpeg" /></p><p>Today let’s talk about “Vibe Coding”, the current trend of writing code by chatting with autonomous AI agents.</p>
<p>I see a lot happening around Vibe Coding, from people getting hacked because their site falls apart, to people bootstrapping entire apps in hours instead of weeks.</p>
<p>I’ve been big into vibe-coding myself and wanted to share some tips and tricks on how to get the most value out of your AI agents.</p>
<p><strong>Who am I and why should you even listen to me?</strong></p>
<p>I’ve been working in tech for the past 14 or so years. 10+ in various roles from Engineer, TL, architect, and 3~4+ years as Engineering Manager in charge of teams of ~10 engineers. I’ve built sensitive high-availability systems used by millions of people, handling all kinds of sensitive data such as financial and banking information.</p>
<p>I want to believe that I&rsquo;m fairly good with coding and designing complex systems, and those skills just happen to apply quite nicely to telling AI agents what to do as well.</p>
<h2 id="wtf-is-vibe-coding">Wtf is “Vibe Coding”?</h2>
<p>Let’s clear this up first - Vibe Coding roughly translates to: Building apps/websites/features by chatting with an AI agent instead of writing code yourself.</p>
<p>Vibe Coding can be asking an AI agent to do simple edits like “Add a new function that does X to this file”, or complex multi-file features in the lines of “Add a new button to this page that does X”.</p>
<p>
<img class="img-fluid" src="./image-20250507234139653.png" alt="Vibe Coding with a Gen Alpha AI" />

</p>
<p>In a very rough sense, Vibe Coding is similar to asking an engineer to do something and assigning them a task: The engineer has to figure out what to do, where the files are and how to accomplish the task.</p>
<h3 id="the-problem-with-vibe-coding-context">The problem with Vibe Coding: Context</h3>
<p>Because of how agents work, each task is effectively similar to a new engineer that just joined your team: You have to teach them everything they need to know about your project to onboard them.</p>
<p>Sure, the engineer, if smart enough, can still be productive from day 1, but the likelihood of them doing something they shouldn’t, or re-implementing something that already exists is high. Combine that with the superhuman speed of spitting out code in seconds and you have a recipe for disaster if not supervised: Broken conventions, re-implemented functions, security holes.</p>
<p>
<img class="img-fluid" src="./GpWULVRboAACSTA.jpeg" alt="Vibe Coding going wrong" />

</p>
<p>And the bad thing: Your agent won’t learn by itself. Once ‘onboarded’ it can be productive for the task you’ve given it, but once you create a new session, we have to start from scratch.</p>
<p>In an optimal world, we could just give the agent the entire codebase as context, but with current limitations, that won’t be possible anytime soon. The opposite is true: The more files we add to an agent&rsquo;s context, the more chaotic and unfocused it will get. Not good.</p>
<p><em>Everything</em> competes for context. Even before we write a single word, the context has already been pre-filled with instructions by your editor, available tools and how to use those tools, MCP, etc.</p>
<p>To give you an idea, the system prompts of different editors (remember when the max context window we had was under 4000 tokens?):</p>
<ul>
<li>Cursor: 3900 words</li>
<li>Cline: 2990 words</li>
<li>Copilot: 1400 words</li>
</ul>
<p>So what we have to do: <strong>Onboard an agent as efficiently as possible</strong> - the least amount of tokens that covers <em>everything</em> the agent needs to know.</p>
<p>I&rsquo;ll structure this post into 2 parts: 1 is talking about the basics of getting more quality out of Vibe Coding, part 2 will be going into more advanced task orchestration.</p>
<h2 id="1-the-basics-of-stronger-agents">1. The basics of stronger agents</h2>
<p>In 1. We’ll talk about how to guide agents and bring them up to speed on your code</p>
<h3 id="11-a-strong-entry-point">1.1 A strong entry point</h3>
<p>The first thing I do in every project I work on is to generate a strong entry point for the agent. Think of it as a document that has a condensed, high-information density overview of what the project is about. Previously I called this “LLM.md”, but nowadays it&rsquo;s standardized to <code>AGENTS.md</code> for Codex and lots of editors, <code>CLAUDE.md</code> for Claude Code, or <code>GEMINI.md</code> for Gemini. I usually pick one and symlink all of them to the same file so every tool finds the right context immediately.</p>
<p>I build each part of this document with the question: If I give this document to someone who has never seen the code in here, will they have enough information to research the rest themselves?</p>
<p>Everything not in this document will require the agent to research it on their own, increasing the likelihood of messing something up, missing something altogether or misunderstanding.</p>
<pre tabindex="0"><code>- What is the project
  - 1-2 lines about what the purpose and goal of the project is
- Key architecture
  - “Uses clean architecture”
  - “Uses MVVN”
  - “All data fetching logic is done with react-query”
  - “Uses bun”, “NodeJS 18+”
  - “Uses jest for tests”
- Key commands to interact with the repository
  - Build command: How to build the app “bun build”, “make build”
  - Lint: How to check the code for errors “make lint”, “bun run lint”
  - Test: How to run tests “bun test”, “make test”, etc
- What are the key parts and where to find them 
  - “/app” - NextJS app directory with routing and pages
  - “/app/components” - React components, includes UI
  - “/repository” - Database layer, contains functions for fetching data
  - “/services” - Directory for connectors to external services and APIs
- Very simple guides on key questions, with more documents going in detail
  - “How to write tests”
    - “All tests go into /test and have to use jest as test runner
    - “Always mock with X. All database interaction has to be mocked”
    - “For react-query testing, read xxx.md”
    - “For React component testing, read yyy.md”
  - “How to create a new API route”
    - “App routes always go into /app/routes and have to use Hono”
    - “Routes are to be imported and mounted into the router at /app/routes/routes.tsx”
</code></pre><p>The point of this document is not to be the knowledge dump for everything going on, but more a booster: It gives a quick refresher of what the project is about, where to find what, what the stack and architecture is, and more importantly: Where to continue checking if something is missing.</p>
<p>Thanks to this, an agent doesn&rsquo;t have to re-scan the entire repository and <code>cd+ls</code> a dozen times, but can focus on targeted scans in the directories that it needs to depending on the task.</p>
<h3 id="12-a-strong-ruleset">1.2 A strong ruleset</h3>
<p>On top of the starting point, I have a separate rule file that is very explicit about how this repository should get handled. This usually goes into whatever agent’s rule definition I currently use. For Cursor, that’s <code>.cursor/rules</code>, for Cline that’s <code>.clinerules</code>, for Roo that’s <code>.roo/rules</code>, for Zed that’s <code>.zed/</code> and so on.</p>
<p>A great feature of modern editors is that they allow you to specify <em>when</em> these rules should get imported. For example, in Cursor, you can specify that a rule only applies to <code>*.js</code> files or <code>*.md</code> files. This is a massive context-saver: The agent doesn&rsquo;t need to know about your database conventions when it&rsquo;s just updating a README.</p>
<p>For general rules that apply to the entire repository regardless of file type, I often skip the editor-specific files and put them directly into my entry point files (<code>AGENTS.md</code>, <code>GEMINI.md</code>, or <code>CLAUDE.md</code>). This ensures that no matter which tool I&rsquo;m using, the core guardrails are always present.</p>
<p>While the starting point is for <em>getting the agent onboarded</em>, the rules are for defining constraints the agent must adhere to. So:</p>
<ul>
<li>Code conventions</li>
<li>Test conventions</li>
<li>Architecture decisions</li>
<li>Git &amp; branching rules</li>
<li>Acceptance criteria for all tasks</li>
</ul>
<p>I keep the rule file unformatted, numbered. I can write rules in markdown but often combine this with the starting point document, so we’re losing precious context for each token.</p>
<p>Here’s an example from a recent project:</p>
<pre tabindex="0"><code>1. Follow TDD: Write a test first, make sure the test fails, then implement the feature. See guides: [Component Testing Guide](mdc:docs/component-testing.md) and [React Query Testing](mdc:docs/react-query-testing.md). Place component tests in test/components/*.test.tsx.
2. Always use `bun` as the package manager and build tool. Run tests with `bun test` and format code with `bun format`. Never use other testing frameworks (like vitest). This project uses bun for testing.
3. Always make sure the build passes before committing code.
4. KISS (Keep It Simple, Stupid): Favor the simplest solution that meets the requirements.
5. DRY (Don&#39;t Repeat Yourself): Avoid code duplication. Extract reusable logic into functions or classes.
6. Docstrings: Write clear and concise docstrings for all classes, functions, and methods, explaining their purpose, parameters, and return values.
7. Create a new branch when working on a bigger feature. Small edits don&#39;t need a new branch.
8. Commit messages start with a verb (first letter capitalized). Include a Linear ticket reference (MIC-XXX format) when available. Be descriptive about what changed and why.
9. Always create a design doc first when creating a new feature. Small edits don&#39;t need this. Read docs/guides/design-doc-guide.md on how to write a design doc.
10. Always break down a design doc into small logical tasks that can be given to an engineer. Think stories. Add this to the design doc as a checklist and check them off as you finish work. Commit into your branch after each task is completed.
11. Use path aliases (`@/*`) instead of relative paths.
12. Follow the exact directory structure and import patterns from existing code
13. Run `bun format` after finishing all changes, and `bun run build` after a feature has been completed
14. Always write a log entry in logs/ with what you did. Name it YYYY-MM-DD-feature-name.md.
15. **All database operations must be implemented exclusively in `/lib/microfn` modules.**. **API routes (`/app/api`) and React components must never contain direct Prisma calls or any database logic.**. **API routes must call exported functions from `/lib/microfn` to perform any database operations.**.
16. Always run the vscode task &#34;format&#34;, or if you don&#39;t have access to it, run `bun format` after each task is completed.
17. DO NOT fix lint errors about formatting, spacing, import order manually. Always run a `bun format` first as that may fix most issues. `bun format` will fix: Import order, removing unused imports, spacing, whitespace, punctuation.
18. Use shadcn and tailwind. Always look at other files in the same directory to understand style and structure.
</code></pre><h3 id="13-use-scripts-for-an-automatic-feedback-loop-to-the-agent">1.3 Use scripts for an automatic feedback loop to the agent</h3>
<p>I consider this an absolute essential for Vibe Coding + AI automation, and maybe even more important than the rules we just talked about.</p>
<p>Agents need to have a way to understand when the build is broken, the tests fail or the code is wrong. Linting can do that automatically if your editor+agent combo supports it, but even better to have it editor agnostic.</p>
<p>These are simple scripts and tools I sprinkled into the previous 2 sections already: <code>make build</code>, <code>bun build</code>, <code>bun lint</code>, <code>make test</code>, <code>bun test</code> - you name it.</p>
<p>In my rule I always have something like this:</p>
<pre tabindex="0"><code>14. Run `bun format` after finishing all changes, and `bun run build` after a feature has been completed
</code></pre><p>No matter what, the agent <strong>has</strong> to run the build script after each task, which will immediately report back if something is broken. This feeds back into the agent that something went wrong, and prompts it to fix the problems.</p>
<ul>
<li>In Swift, I do a <code>xcodebuild</code></li>
<li>In Golang, I do a <code>go build</code></li>
<li>In NextJS, I do a <code>next build</code></li>
</ul>
<p>Similarly, I always have a strong formatter active in my projects that reformats <em>everything</em> and enforces coding style. Agents tend to do what they want when it comes to coding style, and instead of adding more and more rules to instruct them about small things (and taking up precious context space), it’s much easier to just re-format everything through <code>prettier</code>, <code>xcformat</code>, <code>mix format</code>, and add that to the acceptance criteria of every task.</p>
<p>If the agent runs <code>bun format &amp;&amp; bun build</code> after each task, I can be sure that the code is formatted correctly and the build runs before a task is considered complete.</p>
<h3 id="14-improving-agents-through-self-improvement-and-reflection">1.4 Improving agents through self-improvement and reflection</h3>
<p>Another good thing to do, to make sure entry point and rules are updated, is to let the agents do it themselves.</p>
<p>I often include an additional rule like the following. Depending on context size I either have this as a separate prompt somewhere that I can reference (aka “read @prompts/self-improve.md”), or part of my ruleset.</p>
<p>If during a session the agent breaks convention and does something I don’t want it to do, I correct it and teach it the correct way. The purpose of the reflection is to review the conversation history up to this point and put the learnings back into the documents we have.</p>
<pre tabindex="0"><code># Self-Improving Cursor Rules Reflection

**Objective:** Offer opportunities to continuously improve `.cursor/rules` or [CLAUDE.md](mdc:CLAUDE.md) based on user interactions and feedback.

**Trigger:** Before completing any task that involved user feedback provided at any point during the conversation, or involved multiple non-trivial steps (e.g., multiple file edits, complex logic generation).

**Process:**

1.  **Offer Reflection:** Ask the user: &#34;Before I complete the task, would you like me to reflect on our interaction and suggest potential improvements to the active `.cursor/rules` or `CLAUDE.md`?&#34;
2.  **Await User Confirmation:** Proceed to completion immediately if the user declines or doesn&#39;t respond affirmatively.
3.  **If User Confirms:**
    a.  **Review Interaction:** Synthesize all feedback provided by the user throughout the entire conversation history for the task. Analyze how this feedback relates to the active rules in `.cursor/rules` or [CLAUDE.md](mdc:CLAUDE.md) and identify areas where modified instructions could have improved the outcome or better aligned with user preferences.
    b.  **Identify Active Rules:** List the specific global and workspace `.cursor/rules` (or CLAUDE.md) files active during the task.
    c.  **Formulate &amp; Propose Improvements:** Generate specific, actionable suggestions for improving the *content* of the relevant active rule files. Prioritize suggestions directly addressing user feedback. Output a changeset that is usable with `edit_file` to write these suggestions into `.cursor/rules` or `CLAUDE.md`, for the user to review. Don&#39;t execute the tool yet..
    d.  **Await User Action on Suggestions:** Ask the user if they agree with the proposed improvements and if they&#39;d like me to apply them *now* using the appropriate tool (`edit_file`). Apply changes if approved, then proceed to complete the task.

**Constraint:** Do not offer reflection if:
*   No `.cursor/rules` or `CLAUDE.md` were active.
*   The task was very simple and involved no feedback.
</code></pre><p>A prompt like this will then cause the agent to offer reflection after an edit session, if any feedback has been given, neat!</p>
<p>
<img class="img-fluid" src="./image-20250507235647830.png" alt="Reflection in action" />

</p>
<p>It’s of course important to not blindly accept all improvements. LLMs just generate text and aren’t properly smart (yet), so if you tell them to improve your rules, they will almost always suggest stuff to add to it even if not needed. Make sure you approach the rule file with care and keep it clean.</p>
<hr>
<p>Follow these tips and you should have a strong foundation for more complex code changes that are consistent with your existing (and evolving) codebase. Especially when working together with other people, across models and tools, agents need to be aligned on the same guardrails to not go completely nuts.</p>
<p><em>In <a href="https://david.coffee/vibe-coding-advanced/">the next post</a>, we’ll dive into more advanced Vibe Coding: how to handle complex tasks, planning, hand-off, and multi-agent workflows.</em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>What I shipped this year (so far)</title>
      <link>https://david.coffee/what-i-shipped-this-year-so-far/</link>
      <pubDate>Sat, 01 Mar 2025 15:00:12 +0700</pubDate>
      
      <guid>https://david.coffee/what-i-shipped-this-year-so-far/</guid><media:content url="https://david.coffee/what-i-shipped-this-year-so-far/achievements.jpg" medium="image" />
          <enclosure url="https://david.coffee/what-i-shipped-this-year-so-far/achievements.jpg" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/what-i-shipped-this-year-so-far/achievements.jpg&#34; /&gt;&lt;/p&gt;&lt;p&gt;Around October last year, I made the decision to resign from my Engineering Manager position at Mercari with the purpose of focusing on building something up on my own. I love Mercari and have only good things to say about this company, but the fire to go my own way was stronger.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s been about 3 months now and while I am slowly chipping away at my projects, I thought it would be a cool idea to reflect on 2025 and celebrate some of the achievements I&amp;rsquo;ve made so far, but also keep myself accountable by working more in the public.&lt;/p&gt;
&lt;p&gt;I made the habit of keeping a log on my reMarkable to avoid running into the mindset of &amp;ldquo;oh no, I haven&amp;rsquo;t done anything and the month is almost over!!&amp;rdquo; (see the cover image above)&lt;/p&gt;
&lt;h2 id=&#34;january&#34;&gt;January&lt;/h2&gt;
&lt;p&gt;January was a good month. I re-focused and made the habit of going to WeWork offices more often to have that separation between work-mode and non-work-mode. When you don&amp;rsquo;t have anyone telling you what to do, it&amp;rsquo;s easy to get lost in the noise of the internet and lose focus.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s been a lot of smaller side quests, more than I wanted, but in the end, it was a fairly productive month.&lt;/p&gt;
&lt;h3 id=&#34;fix-my-japanese&#34;&gt;Fix My Japanese&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://fixmyjapanese.com&#34;&gt;https://fixmyjapanese.com&lt;/a&gt; received a design overhaul, a new logo and lots of tweaking in the back, laying the groundwork for an app version. I hacked a lot on making this app a reality but while the code was finished, it wasn&amp;rsquo;t ready to be released yet.&lt;/p&gt;
&lt;blockquote class=&#34;twitter-tweet&#34;&gt;&lt;p lang=&#34;en&#34; dir=&#34;ltr&#34;&gt;Tried turning &lt;a href=&#34;https://t.co/BqznRdwlq3&#34;&gt;https://t.co/BqznRdwlq3&lt;/a&gt; into an app &lt;a href=&#34;https://t.co/IOrl82qM8m&#34;&gt;pic.twitter.com/IOrl82qM8m&lt;/a&gt;&lt;/p&gt;&amp;mdash; David (@dvcrn) &lt;a href=&#34;https://twitter.com/dvcrn/status/1885287978844774730?ref_src=twsrc%5Etfw&#34;&gt;January 31, 2025&lt;/a&gt;&lt;/blockquote&gt;
&lt;script async src=&#34;https://platform.twitter.com/widgets.js&#34; charset=&#34;utf-8&#34;&gt;&lt;/script&gt;


&lt;p&gt;FMJ also received a new Sensei and further refined the models in the back to be more accurate.&lt;/p&gt;
&lt;img src=&#34;./fixmyjapanese.png&#34; alt=&#34;Fix My Japanese&#34; style=&#34;zoom:33%;&#34; /&gt;
&lt;h3 id=&#34;masked-email-manager&#34;&gt;Masked Email Manager&lt;/h3&gt;
&lt;p&gt;Masked Email Manager received a bigger update and now integrates with Siri Shortcuts, effectively allowing you to create Masked Emails from anywhere - be it macOS or iOS. I use this a lot myself to quickly create new Masked Emails at hotels or on websites.&lt;/p&gt;
&lt;blockquote class=&#34;twitter-tweet&#34;&gt;&lt;p lang=&#34;en&#34; dir=&#34;ltr&#34;&gt;2.1 of Masked Email Manager for ⁦&lt;a href=&#34;https://twitter.com/Fastmail?ref_src=twsrc%5Etfw&#34;&gt;@Fastmail&lt;/a&gt;⁩ with Siri Shortcuts support now on the AppStore &lt;a href=&#34;https://t.co/JxXRUBExH7&#34;&gt;https://t.co/JxXRUBExH7&lt;/a&gt; &lt;a href=&#34;https://t.co/ejqmo0A8I7&#34;&gt;pic.twitter.com/ejqmo0A8I7&lt;/a&gt;&lt;/p&gt;&amp;mdash; David (@dvcrn) &lt;a href=&#34;https://twitter.com/dvcrn/status/1881899626514796601?ref_src=twsrc%5Etfw&#34;&gt;January 22, 2025&lt;/a&gt;&lt;/blockquote&gt;
&lt;script async src=&#34;https://platform.twitter.com/widgets.js&#34; charset=&#34;utf-8&#34;&gt;&lt;/script&gt;


&lt;p&gt;Here&amp;rsquo;s the Shortcut I use currently: &lt;a href=&#34;https://www.icloud.com/shortcuts/e0ee7234b3784d19bf81791c6fbfc98c&#34;&gt;https://www.icloud.com/shortcuts/e0ee7234b3784d19bf81791c6fbfc98c&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;wework-cli-and-desks-app&#34;&gt;WeWork CLI and Desks App&lt;/h3&gt;
&lt;p&gt;My WeWork CLI &lt;a href=&#34;https://github.com/dvcrn/wework-cli&#34;&gt;https://github.com/dvcrn/wework-cli&lt;/a&gt; that I use to book and list my WeWork All Access bookings received a rewrite from Python to Golang to make maintenance a bit easier.&lt;/p&gt;
&lt;img src=&#34;./wework-cli.png&#34; alt=&#34;WeWork CLI&#34; style=&#34;zoom:33%;&#34; /&gt;
&lt;p&gt;I then used it as base to create an entire app that embeds the Golang library and uses it as the core. This started mainly as a fun &amp;ldquo;I wish I had an app that just shows me my upcoming WeWork Desks without having to use the slow WeWork app all the time&amp;rdquo; and actually turned into a pretty nice app.&lt;/p&gt;
&lt;p&gt;No booking support yet, but that&amp;rsquo;s already done locally, just not released yet.&lt;/p&gt;
&lt;img src=&#34;./desks-for-wework.jpeg&#34; alt=&#34;The Desks app&#34; style=&#34;zoom: 25%;&#34; /&gt;
&lt;p&gt;(Technically approved in February, but submitted at the end of January)&lt;/p&gt;
&lt;h3 id=&#34;pocketsmith&#34;&gt;Pocketsmith&lt;/h3&gt;
&lt;p&gt;This year I started going all in on &lt;a href=&#34;https://www.pocketsmith.com&#34;&gt;Pocketsmith&lt;/a&gt; for managing my finances. The reason being that I started traveling more, and keeping track of Japanese, German, Thai, and US finances was starting to get a bit chaotic.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/dvcrn/pocketsmith-go&#34;&gt;Wrote a Golang SDK&lt;/a&gt; for Pocketsmith&lt;/li&gt;
&lt;li&gt;Created a &lt;a href=&#34;https://github.com/dvcrn/pocketsmith-bangkokbank-enrich&#34;&gt;Bangkok Bank Enricher&lt;/a&gt;: Turns Thailand PromptPay records into rich data showing exactly where I made a purchase, instead of the default &amp;ldquo;made a transfer&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Created a &lt;a href=&#34;https://github.com/dvcrn/pocketsmith-moneytree&#34;&gt;Moneytree Importer&lt;/a&gt; for Japanese Banks&lt;/li&gt;
&lt;li&gt;Created a &lt;a href=&#34;https://github.com/dvcrn/pocketsmith-moneyforward&#34;&gt;Moneyforward Importer&lt;/a&gt; for Japanese Banks&lt;/li&gt;
&lt;li&gt;Created a &lt;a href=&#34;https://github.com/dvcrn/pocketsmith-frollo&#34;&gt;Frollo importer&lt;/a&gt; for Australian Banks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I am very happy with how smoothly this went. I now have almost everything in Pocketsmith and I love that I can quickly see how much I spend on what, no matter what I use for spending.&lt;/p&gt;
&lt;h3 id=&#34;small-stuff&#34;&gt;Small stuff&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Migrated &lt;a href=&#34;https://gts.d.sh/@david&#34;&gt;@david@d.sh from mastodon to GoToSocial&lt;/a&gt;. Maintaining a single-user Mastodon server was getting too tedious and I wanted to reduce resources for upkeep. Saved me about $8/mo in resources.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/dvcrn/chainenv&#34;&gt;chainenv&lt;/a&gt;, my CLI to manage shell secrets in Keychain (&lt;code&gt;chainenv set foo bar&lt;/code&gt;) received a rewrite from Python in Golang as well, and with that support to use 1Password as backend (through my &lt;a href=&#34;https://github.com/dvcrn/go-1password-cli&#34;&gt;https://github.com/dvcrn/go-1password-cli&lt;/a&gt; that was already in Golang). I&amp;rsquo;m now using this for all my projects to simply have &lt;code&gt;export FOO=(chainenv FOO)&lt;/code&gt; in &lt;code&gt;.envrc&lt;/code&gt; and not have secrets laying around on my machine.&lt;/p&gt;
&lt;img src=&#34;./chainenv.jpeg&#34; alt=&#34;chainenv&#34; style=&#34;zoom:50%;&#34; /&gt;
&lt;ul&gt;
&lt;li&gt;I&amp;rsquo;ve created a custom GPT to find cool upcoming music events in Tokyo called &amp;ldquo;&lt;a href=&#34;https://chatgpt.com/g/g-6780ef5b49488191a1351fb45e6fb494-tokyo-club-event-buddy-dong-jing-kurahuihentokun&#34;&gt;Tokyo Club Event Buddy&lt;/a&gt;&amp;rdquo;. It&amp;rsquo;s powered by a custom aggregator that pulls from Zaiko, iFlyer, and RA, then exposes that to ChatGPT.&lt;/li&gt;
&lt;/ul&gt;
&lt;img src=&#34;./tokyo-club-event-buddy.jpeg&#34; alt=&#34;Tokyo Club Event Buddy GPT&#34; style=&#34;zoom:50%;&#34; /&gt;
&lt;ul&gt;
&lt;li&gt;Started learning about Cloudflare Zero Trust and moved most of my homeserver + private services onto CF tunnels next to Tailscale. For example, I can now access my Homeassistant through &lt;a href=&#34;https://hass.d.sh&#34;&gt;https://hass.d.sh&lt;/a&gt;, which requires Okta authentication, or when I&amp;rsquo;m on WARP tunnel, just lets me through without auth. Saved me $10/mo in loadbalancer+external IP cost and another $10/mo on the homeassistant-cloud subscription&lt;/li&gt;
&lt;li&gt;I&amp;rsquo;ve set up my own matrix server (&lt;a href=&#34;https://conduwuit.d.sh&#34;&gt;https://conduwuit.d.sh&lt;/a&gt;, also exposed through CF tunnels), and added a bunch of bridges to it. So all my Facebook/Instagram/LinkedIn/WhatsApp/Google Messages/iMessage/etc. are now on my self-hosted server, fully encrypted.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Created a fork of Cinny (matrix client) with changes to room sorting, &lt;a href=&#34;https://github.com/dvcrn/davids-cinny&#34;&gt;https://github.com/dvcrn/davids-cinny&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A lot of these side quests were &lt;a href=&#34;https://www.hanselman.com/blog/yak-shaving-defined-ill-get-that-done-as-soon-as-i-shave-this-yak&#34;&gt;Yak-shaving&lt;/a&gt;, but I had fun doing it nevertheless.&lt;/p&gt;
&lt;h2 id=&#34;february&#34;&gt;February&lt;/h2&gt;
&lt;p&gt;Looking at my February achievements, it was less small stuff and more focusing on a few bigger projects. However, I also travelled more. For example, this post has been written from a cafe in Phnom Penh.&lt;/p&gt;
&lt;h3 id=&#34;fix-my-japanese-1&#34;&gt;Fix My Japanese&lt;/h3&gt;
&lt;p&gt;The month kicked off with my release of Fix My Japanese on iOS. Most of the work was done in January, but I submitted it in February and it got approved in February, so here we are!&lt;/p&gt;
&lt;blockquote class=&#34;twitter-tweet&#34;&gt;&lt;p lang=&#34;en&#34; dir=&#34;ltr&#34;&gt;Fix My Japanese is now on the AppStore! 🥳&lt;br&gt;Very much a 1.0 but public is public&lt;a href=&#34;https://t.co/ZvYwtAFpr9&#34;&gt;https://t.co/ZvYwtAFpr9&lt;/a&gt; &lt;a href=&#34;https://t.co/5nSWXSyT7W&#34;&gt;pic.twitter.com/5nSWXSyT7W&lt;/a&gt;&lt;/p&gt;&amp;mdash; David (@dvcrn) &lt;a href=&#34;https://twitter.com/dvcrn/status/1889454914616828376?ref_src=twsrc%5Etfw&#34;&gt;February 11, 2025&lt;/a&gt;&lt;/blockquote&gt;
&lt;script async src=&#34;https://platform.twitter.com/widgets.js&#34; charset=&#34;utf-8&#34;&gt;&lt;/script&gt;


&lt;p&gt;This is very much a 1.0 but I am quite happy with how it turned out. I am not an iOS developer and it&amp;rsquo;s nice to slowly get the hang of things (after learning a lot from Desks and Masked Email Manager).&lt;/p&gt;
&lt;p&gt;On the web, after getting some user feedback, I added a new slider for picking the politeness level, and added a new &amp;ldquo;Keigo&amp;rdquo; level for more formal Japanese.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./fixmyjapanese-politeness.png&#34; alt=&#34;Fix My Japanese Politeness Slider&#34; /&gt;

&lt;/p&gt;
&lt;h3 id=&#34;microfn&#34;&gt;Microfn&lt;/h3&gt;
&lt;p&gt;Most of my time got taken by &lt;a href=&#34;https://microfn.dev&#34;&gt;https://microfn.dev&lt;/a&gt;, a new project I&amp;rsquo;m working on that I can&amp;rsquo;t talk about much yet. Hopefully the first public release will be up in March. Not much more to say at this point.&lt;/p&gt;
&lt;h3 id=&#34;tripit--n8n&#34;&gt;TripIt &amp;amp; n8n&lt;/h3&gt;
&lt;p&gt;My &lt;a href=&#34;https://github.com/dvcrn/n8n-nodes-tripit&#34;&gt;TripIt extension for n8n&lt;/a&gt; received a bigger rewrite to fix some auth issues and now properly implements oauth (after learning how to do it with the WeWork app). I also added &amp;ldquo;Transport&amp;rdquo; support.&lt;/p&gt;
&lt;p&gt;While definitely in the Yak-shaving territory, this allowed me to build fully autonomous agents that manage my trips for me. For example, I have agents running that read my emails, extract flight/hotel/activity/events, find matching trips and file them accordingly.&lt;/p&gt;
&lt;p&gt;This information is then made available in my calendar, given me an up-to-date overview of what is happening when.&lt;/p&gt;
&lt;img src=&#34;./calendar.png&#34; alt=&#34;Calendar managed by TripIt&#34; style=&#34;zoom: 33%;&#34; /&gt;
&lt;p&gt;The agents also attach PDF documents to the segments, so if I want to have my booking confirmation for a flight, I can just open TripIt, select the trip, and the flight information is there.&lt;/p&gt;
&lt;p&gt;The cool thing is that I can hook this up to Matrix, so the agent will tell me whenever it did something, like having an AI-powered PA.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./n8n1.png&#34; alt=&#34;n8n agent in action&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./n8n2.png&#34; alt=&#34;n8n agent in action&#34; /&gt;

&lt;/p&gt;
&lt;h3 id=&#34;smaller-stuff&#34;&gt;Smaller stuff&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Siri Shortcuts MCP - A model context provider server that exposes all Siri Shortcuts to LLMs. So I can ask Claude to get information about anything that I have Shortcuts for, do things like &amp;ldquo;activate Cloudflare WARP tunnel&amp;rdquo;, &amp;ldquo;get my IP&amp;rdquo;, and so on.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./claude-mcp.jpeg&#34; alt=&#34;Claude with Shortcuts MCP&#34; /&gt;

&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Created an app for more AI actions through Siri Shortcuts (using the learnings from Masked Email Manager). Nothing out yet, but it&amp;rsquo;s almost done.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Noodled more into music with Ableton. Nothing to show yet, but I&amp;rsquo;m learning.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Made a habit to take more photos with my camera and drag my ass outside.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;February felt like it went by in a breeze. Reflecting on everything now, I&amp;rsquo;m surprised that I have done so little, and that microfn ended up consuming so much of my time.&lt;/p&gt;
&lt;h2 id=&#34;other-thoughts&#34;&gt;Other thoughts&lt;/h2&gt;
&lt;p&gt;I do not regret going my own path, but I also need to re-focus on bigger projects and keep distractions to a minimum. The goal is to build something that makes money, and so far a lot of the things I did aren&amp;rsquo;t getting me closer to that goal.&lt;/p&gt;
&lt;p&gt;AI is reaching critical saturation, I see people using ChatGPT/Microsoft Copilot everywhere, from trains to cafes, and autonomous coding agents like Cursor, Windsurf, Cline and Roo are getting incredibly good. Github Copilot even created their own coding agent that&amp;rsquo;s available in Visual Studio Code Insiders as preview.&lt;/p&gt;
&lt;p&gt;I love that AI is making me 10x more productive, but I&amp;rsquo;d lie if I said I&amp;rsquo;m not worried about what it&amp;rsquo;ll do to my career. Junior engineers will probably have it the hardest, but we&amp;rsquo;re reaching a point where apps can get created in days without much coding experience, so I&amp;rsquo;m expecting less demand for engineers in general, but also a rapid flood of new apps and services hitting the market.&lt;/p&gt;
&lt;p&gt;Only time will tell what&amp;rsquo;ll happen next and where the IT sector is going.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/what-i-shipped-this-year-so-far/achievements.jpg" /></p><p>Around October last year, I made the decision to resign from my Engineering Manager position at Mercari with the purpose of focusing on building something up on my own. I love Mercari and have only good things to say about this company, but the fire to go my own way was stronger.</p>
<p>It&rsquo;s been about 3 months now and while I am slowly chipping away at my projects, I thought it would be a cool idea to reflect on 2025 and celebrate some of the achievements I&rsquo;ve made so far, but also keep myself accountable by working more in the public.</p>
<p>I made the habit of keeping a log on my reMarkable to avoid running into the mindset of &ldquo;oh no, I haven&rsquo;t done anything and the month is almost over!!&rdquo; (see the cover image above)</p>
<h2 id="january">January</h2>
<p>January was a good month. I re-focused and made the habit of going to WeWork offices more often to have that separation between work-mode and non-work-mode. When you don&rsquo;t have anyone telling you what to do, it&rsquo;s easy to get lost in the noise of the internet and lose focus.</p>
<p>There&rsquo;s been a lot of smaller side quests, more than I wanted, but in the end, it was a fairly productive month.</p>
<h3 id="fix-my-japanese">Fix My Japanese</h3>
<p><a href="https://fixmyjapanese.com">https://fixmyjapanese.com</a> received a design overhaul, a new logo and lots of tweaking in the back, laying the groundwork for an app version. I hacked a lot on making this app a reality but while the code was finished, it wasn&rsquo;t ready to be released yet.</p>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Tried turning <a href="https://t.co/BqznRdwlq3">https://t.co/BqznRdwlq3</a> into an app <a href="https://t.co/IOrl82qM8m">pic.twitter.com/IOrl82qM8m</a></p>&mdash; David (@dvcrn) <a href="https://twitter.com/dvcrn/status/1885287978844774730?ref_src=twsrc%5Etfw">January 31, 2025</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


<p>FMJ also received a new Sensei and further refined the models in the back to be more accurate.</p>
<img src="./fixmyjapanese.png" alt="Fix My Japanese" style="zoom:33%;" />
<h3 id="masked-email-manager">Masked Email Manager</h3>
<p>Masked Email Manager received a bigger update and now integrates with Siri Shortcuts, effectively allowing you to create Masked Emails from anywhere - be it macOS or iOS. I use this a lot myself to quickly create new Masked Emails at hotels or on websites.</p>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">2.1 of Masked Email Manager for ⁦<a href="https://twitter.com/Fastmail?ref_src=twsrc%5Etfw">@Fastmail</a>⁩ with Siri Shortcuts support now on the AppStore <a href="https://t.co/JxXRUBExH7">https://t.co/JxXRUBExH7</a> <a href="https://t.co/ejqmo0A8I7">pic.twitter.com/ejqmo0A8I7</a></p>&mdash; David (@dvcrn) <a href="https://twitter.com/dvcrn/status/1881899626514796601?ref_src=twsrc%5Etfw">January 22, 2025</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


<p>Here&rsquo;s the Shortcut I use currently: <a href="https://www.icloud.com/shortcuts/e0ee7234b3784d19bf81791c6fbfc98c">https://www.icloud.com/shortcuts/e0ee7234b3784d19bf81791c6fbfc98c</a></p>
<h3 id="wework-cli-and-desks-app">WeWork CLI and Desks App</h3>
<p>My WeWork CLI <a href="https://github.com/dvcrn/wework-cli">https://github.com/dvcrn/wework-cli</a> that I use to book and list my WeWork All Access bookings received a rewrite from Python to Golang to make maintenance a bit easier.</p>
<img src="./wework-cli.png" alt="WeWork CLI" style="zoom:33%;" />
<p>I then used it as base to create an entire app that embeds the Golang library and uses it as the core. This started mainly as a fun &ldquo;I wish I had an app that just shows me my upcoming WeWork Desks without having to use the slow WeWork app all the time&rdquo; and actually turned into a pretty nice app.</p>
<p>No booking support yet, but that&rsquo;s already done locally, just not released yet.</p>
<img src="./desks-for-wework.jpeg" alt="The Desks app" style="zoom: 25%;" />
<p>(Technically approved in February, but submitted at the end of January)</p>
<h3 id="pocketsmith">Pocketsmith</h3>
<p>This year I started going all in on <a href="https://www.pocketsmith.com">Pocketsmith</a> for managing my finances. The reason being that I started traveling more, and keeping track of Japanese, German, Thai, and US finances was starting to get a bit chaotic.</p>
<ul>
<li><a href="https://github.com/dvcrn/pocketsmith-go">Wrote a Golang SDK</a> for Pocketsmith</li>
<li>Created a <a href="https://github.com/dvcrn/pocketsmith-bangkokbank-enrich">Bangkok Bank Enricher</a>: Turns Thailand PromptPay records into rich data showing exactly where I made a purchase, instead of the default &ldquo;made a transfer&rdquo;</li>
<li>Created a <a href="https://github.com/dvcrn/pocketsmith-moneytree">Moneytree Importer</a> for Japanese Banks</li>
<li>Created a <a href="https://github.com/dvcrn/pocketsmith-moneyforward">Moneyforward Importer</a> for Japanese Banks</li>
<li>Created a <a href="https://github.com/dvcrn/pocketsmith-frollo">Frollo importer</a> for Australian Banks</li>
</ul>
<p>I am very happy with how smoothly this went. I now have almost everything in Pocketsmith and I love that I can quickly see how much I spend on what, no matter what I use for spending.</p>
<h3 id="small-stuff">Small stuff</h3>
<ul>
<li>
<p>Migrated <a href="https://gts.d.sh/@david">@david@d.sh from mastodon to GoToSocial</a>. Maintaining a single-user Mastodon server was getting too tedious and I wanted to reduce resources for upkeep. Saved me about $8/mo in resources.</p>
</li>
<li>
<p><a href="https://github.com/dvcrn/chainenv">chainenv</a>, my CLI to manage shell secrets in Keychain (<code>chainenv set foo bar</code>) received a rewrite from Python in Golang as well, and with that support to use 1Password as backend (through my <a href="https://github.com/dvcrn/go-1password-cli">https://github.com/dvcrn/go-1password-cli</a> that was already in Golang). I&rsquo;m now using this for all my projects to simply have <code>export FOO=(chainenv FOO)</code> in <code>.envrc</code> and not have secrets laying around on my machine.</p>
<img src="./chainenv.jpeg" alt="chainenv" style="zoom:50%;" />
<ul>
<li>I&rsquo;ve created a custom GPT to find cool upcoming music events in Tokyo called &ldquo;<a href="https://chatgpt.com/g/g-6780ef5b49488191a1351fb45e6fb494-tokyo-club-event-buddy-dong-jing-kurahuihentokun">Tokyo Club Event Buddy</a>&rdquo;. It&rsquo;s powered by a custom aggregator that pulls from Zaiko, iFlyer, and RA, then exposes that to ChatGPT.</li>
</ul>
<img src="./tokyo-club-event-buddy.jpeg" alt="Tokyo Club Event Buddy GPT" style="zoom:50%;" />
<ul>
<li>Started learning about Cloudflare Zero Trust and moved most of my homeserver + private services onto CF tunnels next to Tailscale. For example, I can now access my Homeassistant through <a href="https://hass.d.sh">https://hass.d.sh</a>, which requires Okta authentication, or when I&rsquo;m on WARP tunnel, just lets me through without auth. Saved me $10/mo in loadbalancer+external IP cost and another $10/mo on the homeassistant-cloud subscription</li>
<li>I&rsquo;ve set up my own matrix server (<a href="https://conduwuit.d.sh">https://conduwuit.d.sh</a>, also exposed through CF tunnels), and added a bunch of bridges to it. So all my Facebook/Instagram/LinkedIn/WhatsApp/Google Messages/iMessage/etc. are now on my self-hosted server, fully encrypted.</li>
</ul>
</li>
<li>
<p>Created a fork of Cinny (matrix client) with changes to room sorting, <a href="https://github.com/dvcrn/davids-cinny">https://github.com/dvcrn/davids-cinny</a></p>
</li>
</ul>
<p>A lot of these side quests were <a href="https://www.hanselman.com/blog/yak-shaving-defined-ill-get-that-done-as-soon-as-i-shave-this-yak">Yak-shaving</a>, but I had fun doing it nevertheless.</p>
<h2 id="february">February</h2>
<p>Looking at my February achievements, it was less small stuff and more focusing on a few bigger projects. However, I also travelled more. For example, this post has been written from a cafe in Phnom Penh.</p>
<h3 id="fix-my-japanese-1">Fix My Japanese</h3>
<p>The month kicked off with my release of Fix My Japanese on iOS. Most of the work was done in January, but I submitted it in February and it got approved in February, so here we are!</p>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Fix My Japanese is now on the AppStore! 🥳<br>Very much a 1.0 but public is public<a href="https://t.co/ZvYwtAFpr9">https://t.co/ZvYwtAFpr9</a> <a href="https://t.co/5nSWXSyT7W">pic.twitter.com/5nSWXSyT7W</a></p>&mdash; David (@dvcrn) <a href="https://twitter.com/dvcrn/status/1889454914616828376?ref_src=twsrc%5Etfw">February 11, 2025</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


<p>This is very much a 1.0 but I am quite happy with how it turned out. I am not an iOS developer and it&rsquo;s nice to slowly get the hang of things (after learning a lot from Desks and Masked Email Manager).</p>
<p>On the web, after getting some user feedback, I added a new slider for picking the politeness level, and added a new &ldquo;Keigo&rdquo; level for more formal Japanese.</p>
<p>
<img class="img-fluid" src="./fixmyjapanese-politeness.png" alt="Fix My Japanese Politeness Slider" />

</p>
<h3 id="microfn">Microfn</h3>
<p>Most of my time got taken by <a href="https://microfn.dev">https://microfn.dev</a>, a new project I&rsquo;m working on that I can&rsquo;t talk about much yet. Hopefully the first public release will be up in March. Not much more to say at this point.</p>
<h3 id="tripit--n8n">TripIt &amp; n8n</h3>
<p>My <a href="https://github.com/dvcrn/n8n-nodes-tripit">TripIt extension for n8n</a> received a bigger rewrite to fix some auth issues and now properly implements oauth (after learning how to do it with the WeWork app). I also added &ldquo;Transport&rdquo; support.</p>
<p>While definitely in the Yak-shaving territory, this allowed me to build fully autonomous agents that manage my trips for me. For example, I have agents running that read my emails, extract flight/hotel/activity/events, find matching trips and file them accordingly.</p>
<p>This information is then made available in my calendar, given me an up-to-date overview of what is happening when.</p>
<img src="./calendar.png" alt="Calendar managed by TripIt" style="zoom: 33%;" />
<p>The agents also attach PDF documents to the segments, so if I want to have my booking confirmation for a flight, I can just open TripIt, select the trip, and the flight information is there.</p>
<p>The cool thing is that I can hook this up to Matrix, so the agent will tell me whenever it did something, like having an AI-powered PA.</p>
<p>
<img class="img-fluid" src="./n8n1.png" alt="n8n agent in action" />

</p>
<p>
<img class="img-fluid" src="./n8n2.png" alt="n8n agent in action" />

</p>
<h3 id="smaller-stuff">Smaller stuff</h3>
<ul>
<li>
<p>Siri Shortcuts MCP - A model context provider server that exposes all Siri Shortcuts to LLMs. So I can ask Claude to get information about anything that I have Shortcuts for, do things like &ldquo;activate Cloudflare WARP tunnel&rdquo;, &ldquo;get my IP&rdquo;, and so on.</p>
<p>
<img class="img-fluid" src="./claude-mcp.jpeg" alt="Claude with Shortcuts MCP" />

</p>
</li>
<li>
<p>Created an app for more AI actions through Siri Shortcuts (using the learnings from Masked Email Manager). Nothing out yet, but it&rsquo;s almost done.</p>
</li>
<li>
<p>Noodled more into music with Ableton. Nothing to show yet, but I&rsquo;m learning.</p>
</li>
<li>
<p>Made a habit to take more photos with my camera and drag my ass outside.</p>
</li>
</ul>
<p>February felt like it went by in a breeze. Reflecting on everything now, I&rsquo;m surprised that I have done so little, and that microfn ended up consuming so much of my time.</p>
<h2 id="other-thoughts">Other thoughts</h2>
<p>I do not regret going my own path, but I also need to re-focus on bigger projects and keep distractions to a minimum. The goal is to build something that makes money, and so far a lot of the things I did aren&rsquo;t getting me closer to that goal.</p>
<p>AI is reaching critical saturation, I see people using ChatGPT/Microsoft Copilot everywhere, from trains to cafes, and autonomous coding agents like Cursor, Windsurf, Cline and Roo are getting incredibly good. Github Copilot even created their own coding agent that&rsquo;s available in Visual Studio Code Insiders as preview.</p>
<p>I love that AI is making me 10x more productive, but I&rsquo;d lie if I said I&rsquo;m not worried about what it&rsquo;ll do to my career. Junior engineers will probably have it the hardest, but we&rsquo;re reaching a point where apps can get created in days without much coding experience, so I&rsquo;m expecting less demand for engineers in general, but also a rapid flood of new apps and services hitting the market.</p>
<p>Only time will tell what&rsquo;ll happen next and where the IT sector is going.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>FixMyJapanese - Let AI help you improve your Japanese</title>
      <link>https://david.coffee/fix-my-japanese/</link>
      <pubDate>Tue, 04 Feb 2025 10:00:00 +0900</pubDate>
      
      <guid>https://david.coffee/fix-my-japanese/</guid><media:content url="https://david.coffee/fix-my-japanese/banner.jpg" medium="image" />
          <enclosure url="https://david.coffee/fix-my-japanese/banner.jpg" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/fix-my-japanese/banner.jpg&#34; /&gt;&lt;/p&gt;&lt;p&gt;Japanese grammar can be very particular, especially when dealing with particles and the more polite forms, like in emails. To this day, I am still making mistakes without anyone ever correcting me!! I got frustrated so back then, before ChatGPT and all the fancy custom GPTs were a thing, I thought it would be a cool idea to hook up GPT3 (yes GPT3, not even ChatGPT) to an UI and ended up building this little tool for myself, calling it fittingly, &amp;ldquo;FixMyJapanese.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s gotten better over time, and now it&amp;rsquo;s a free service that anyone can use at &lt;a href=&#34;https://fixmyjapanese.com&#34;&gt;https://fixmyjapanese.com&lt;/a&gt;.  Check it out!&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;screenshot.png&#34; alt=&#34;screenshot&#34; /&gt;

&lt;/p&gt;
&lt;h2 id=&#34;what-exactly-is-this&#34;&gt;What exactly is this?&lt;/h2&gt;
&lt;p&gt;Fix My Japanese is an AI-powered service that helps you figure out where you&amp;rsquo;re going wrong with your Japanese and learn from your mistakes.  (yeah, yeah, I know, everything is AI-powered these days, but to my defense, this existed before the AI craze started)&lt;/p&gt;
&lt;p&gt;Basically, you give it some Japanese text, and it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gives you back a corrected version of what you wrote.&lt;/li&gt;
&lt;li&gt;Tells you exactly what the grammar mistakes are and explains why they&amp;rsquo;re wrong.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The idea is that by seeing these corrections and explanations, you&amp;rsquo;ll start to recognize the patterns and make those mistakes less and less.&lt;/p&gt;
&lt;p&gt;It even has different politeness levels you can play with:&lt;/p&gt;
&lt;img src=&#34;./politeness-selector.png&#34; alt=&#34;Politeness selector&#34; style=&#34;zoom:50%;&#34; /&gt;
&lt;p&gt;The &amp;ldquo;Kansai&amp;rdquo; option isn&amp;rsquo;t really a politeness thing, but as a Tokyoite, I find it hilarious to see how &lt;code&gt;日本語を話せません。ごめんなさい&lt;/code&gt; turns into &lt;code&gt;日本語話せへん。すまんな。&lt;/code&gt;.  It&amp;rsquo;s a great party trick if you&amp;rsquo;re hanging out with friends from Osaka. 😉&lt;/p&gt;
&lt;h2 id=&#34;different-senseis-different-styles&#34;&gt;Different Senseis, different styles&lt;/h2&gt;
&lt;p&gt;There are also different &amp;ldquo;Senseis&amp;rdquo; to choose from, each with their own style:&lt;/p&gt;
&lt;img src=&#34;./sensei-selector.png&#34; alt=&#34;Sensei selector&#34; style=&#34;zoom:50%;&#34; /&gt;
&lt;p&gt;Each Sensei will give you slightly different corrections and explanations.  For example, let&amp;rsquo;s say you wrote:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;申し訳ございませんが、僕はこのリンゴを聴こえません
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Yuki might say:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;申し訳ございませんが、僕はこのリンゴを見ることができません。&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;While Sakura might say:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;申し訳ございませんが、私はこのリンゴを食べられません&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;The Senseis change and get better over time, so try them all out and see which one you like best!&lt;/p&gt;
&lt;h2 id=&#34;tell-me-what-you-think&#34;&gt;Tell me what you think!&lt;/h2&gt;
&lt;p&gt;If this is of value to you and helps you with your Japanese, shoot me an email at &lt;a href=&#34;mailto:contact@fixmyjapanese.com&#34;&gt;contact@fixmyjapanese.com&lt;/a&gt; and tell me what you think!&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/fix-my-japanese/banner.jpg" /></p><p>Japanese grammar can be very particular, especially when dealing with particles and the more polite forms, like in emails. To this day, I am still making mistakes without anyone ever correcting me!! I got frustrated so back then, before ChatGPT and all the fancy custom GPTs were a thing, I thought it would be a cool idea to hook up GPT3 (yes GPT3, not even ChatGPT) to an UI and ended up building this little tool for myself, calling it fittingly, &ldquo;FixMyJapanese.&rdquo;</p>
<p>It&rsquo;s gotten better over time, and now it&rsquo;s a free service that anyone can use at <a href="https://fixmyjapanese.com">https://fixmyjapanese.com</a>.  Check it out!</p>
<p>
<img class="img-fluid" src="screenshot.png" alt="screenshot" />

</p>
<h2 id="what-exactly-is-this">What exactly is this?</h2>
<p>Fix My Japanese is an AI-powered service that helps you figure out where you&rsquo;re going wrong with your Japanese and learn from your mistakes.  (yeah, yeah, I know, everything is AI-powered these days, but to my defense, this existed before the AI craze started)</p>
<p>Basically, you give it some Japanese text, and it:</p>
<ul>
<li>Gives you back a corrected version of what you wrote.</li>
<li>Tells you exactly what the grammar mistakes are and explains why they&rsquo;re wrong.</li>
</ul>
<p>The idea is that by seeing these corrections and explanations, you&rsquo;ll start to recognize the patterns and make those mistakes less and less.</p>
<p>It even has different politeness levels you can play with:</p>
<img src="./politeness-selector.png" alt="Politeness selector" style="zoom:50%;" />
<p>The &ldquo;Kansai&rdquo; option isn&rsquo;t really a politeness thing, but as a Tokyoite, I find it hilarious to see how <code>日本語を話せません。ごめんなさい</code> turns into <code>日本語話せへん。すまんな。</code>.  It&rsquo;s a great party trick if you&rsquo;re hanging out with friends from Osaka. 😉</p>
<h2 id="different-senseis-different-styles">Different Senseis, different styles</h2>
<p>There are also different &ldquo;Senseis&rdquo; to choose from, each with their own style:</p>
<img src="./sensei-selector.png" alt="Sensei selector" style="zoom:50%;" />
<p>Each Sensei will give you slightly different corrections and explanations.  For example, let&rsquo;s say you wrote:</p>
<pre tabindex="0"><code>申し訳ございませんが、僕はこのリンゴを聴こえません
</code></pre><p>Yuki might say:</p>
<blockquote>
<p>申し訳ございませんが、僕はこのリンゴを見ることができません。</p></blockquote>
<p>While Sakura might say:</p>
<blockquote>
<p>申し訳ございませんが、私はこのリンゴを食べられません</p></blockquote>
<p>The Senseis change and get better over time, so try them all out and see which one you like best!</p>
<h2 id="tell-me-what-you-think">Tell me what you think!</h2>
<p>If this is of value to you and helps you with your Japanese, shoot me an email at <a href="mailto:contact@fixmyjapanese.com">contact@fixmyjapanese.com</a> and tell me what you think!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Masked Email Manager 2.1 With Shortcuts Support</title>
      <link>https://david.coffee/masked-email-manager-210-shortcuts/</link>
      <pubDate>Wed, 22 Jan 2025 10:00:00 +0900</pubDate>
      
      <guid>https://david.coffee/masked-email-manager-210-shortcuts/</guid><media:content url="https://david.coffee/masked-email-manager-210-shortcuts/teaser.jpg" medium="image" />
          <enclosure url="https://david.coffee/masked-email-manager-210-shortcuts/teaser.jpg" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/masked-email-manager-210-shortcuts/teaser.jpg&#34; /&gt;&lt;/p&gt;&lt;p&gt;Here&amp;rsquo;s a fresh new release of my little Masked Email Manager app that I &lt;a href=&#34;https://david.coffee/fastmail-masked-email-manager-release/&#34;&gt;previously talked about&lt;/a&gt;. The major new feature is Siri Shortcuts integration, which makes it possible to create Masked Emails without even opening the app.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://apps.apple.com/jp/app/masked-email-manager/id6443853807?l=en-US&#34;&gt;Get the latest version on the App Store&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./shortcut-example.png&#34; alt=&#34;Example Shortcut&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;This was my first time working with Siri Shortcuts on the development side, so it was interesting to see what happens behind the scenes when a Shortcut triggers.&lt;/p&gt;
&lt;p&gt;Some cool stuff you can do with this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create Masked Emails for websites without using 1Password or Keyboard Maestro&lt;/li&gt;
&lt;li&gt;Quickly trigger from Widgets or Control Center items on iOS, for example when in a hotel&lt;/li&gt;
&lt;li&gt;Use Shortery to bind a keyboard shortcut to triggering the creation of a new Masked Email&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;rsquo;s a quick example Shortcut that reads what&amp;rsquo;s on screen (e.g., the website of a browser), parses it, and if it finds a domain it directly creates a Masked Email that it then copies to clipboard:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.icloud.com/shortcuts/e0ee7234b3784d19bf81791c6fbfc98c&#34;&gt;Download Example Shortcut&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;an-actual-native-app-since-20-with-a-twist&#34;&gt;An actual native app since 2.0 (with a twist)&lt;/h2&gt;
&lt;p&gt;The app has undergone significant changes since I last wrote about it. With version 2.0, the app was completely rewritten in SwiftUI, saying farewell to React Native.&lt;/p&gt;
&lt;p&gt;The cool thing is that it&amp;rsquo;s still powered by the same tech, mainly my &lt;a href=&#34;https://github.com/dvcrn/maskedemail-cli&#34;&gt;maskedemail-cli&lt;/a&gt;. Previously, I compiled it from Golang into WASM (which didn&amp;rsquo;t go so well), then into GopherJS and embedded it into the React Native app.&lt;/p&gt;
&lt;p&gt;Now with everything being Swift, we no longer need GopherJS. Instead, it&amp;rsquo;s using &lt;a href=&#34;https://golang.org/x/mobile&#34;&gt;gomobile&lt;/a&gt; to build a native iOS .xcframework which is then embedded into the app.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://apps.apple.com/jp/app/masked-email-manager/id6443853807?l=en-US&#34;&gt;Get the app on the App Store&lt;/a&gt;&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/masked-email-manager-210-shortcuts/teaser.jpg" /></p><p>Here&rsquo;s a fresh new release of my little Masked Email Manager app that I <a href="https://david.coffee/fastmail-masked-email-manager-release/">previously talked about</a>. The major new feature is Siri Shortcuts integration, which makes it possible to create Masked Emails without even opening the app.</p>
<p><a href="https://apps.apple.com/jp/app/masked-email-manager/id6443853807?l=en-US">Get the latest version on the App Store</a></p>
<p>
<img class="img-fluid" src="./shortcut-example.png" alt="Example Shortcut" />

</p>
<p>This was my first time working with Siri Shortcuts on the development side, so it was interesting to see what happens behind the scenes when a Shortcut triggers.</p>
<p>Some cool stuff you can do with this:</p>
<ul>
<li>Create Masked Emails for websites without using 1Password or Keyboard Maestro</li>
<li>Quickly trigger from Widgets or Control Center items on iOS, for example when in a hotel</li>
<li>Use Shortery to bind a keyboard shortcut to triggering the creation of a new Masked Email</li>
</ul>
<p>Here&rsquo;s a quick example Shortcut that reads what&rsquo;s on screen (e.g., the website of a browser), parses it, and if it finds a domain it directly creates a Masked Email that it then copies to clipboard:</p>
<p><a href="https://www.icloud.com/shortcuts/e0ee7234b3784d19bf81791c6fbfc98c">Download Example Shortcut</a></p>
<h2 id="an-actual-native-app-since-20-with-a-twist">An actual native app since 2.0 (with a twist)</h2>
<p>The app has undergone significant changes since I last wrote about it. With version 2.0, the app was completely rewritten in SwiftUI, saying farewell to React Native.</p>
<p>The cool thing is that it&rsquo;s still powered by the same tech, mainly my <a href="https://github.com/dvcrn/maskedemail-cli">maskedemail-cli</a>. Previously, I compiled it from Golang into WASM (which didn&rsquo;t go so well), then into GopherJS and embedded it into the React Native app.</p>
<p>Now with everything being Swift, we no longer need GopherJS. Instead, it&rsquo;s using <a href="https://golang.org/x/mobile">gomobile</a> to build a native iOS .xcframework which is then embedded into the app.</p>
<p><a href="https://apps.apple.com/jp/app/masked-email-manager/id6443853807?l=en-US">Get the app on the App Store</a></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Which Japanese SIM to pick? An IT professional&#39;s opinion (Ahamo, Rakuten Mobile, Povo)</title>
      <link>https://david.coffee/japanese-sim-cards/</link>
      <pubDate>Fri, 24 Nov 2023 10:00:00 +0900</pubDate>
      
      <guid>https://david.coffee/japanese-sim-cards/</guid><media:content url="https://david.coffee/japanese-sim-cards/banner.png" medium="image" />
          <enclosure url="https://david.coffee/japanese-sim-cards/banner.png" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/japanese-sim-cards/banner.png&#34; /&gt;&lt;/p&gt;&lt;p&gt;I was discussing Japanese SIM cards with a friend the other day and noticed that there aren&amp;rsquo;t really any English resources to help people choose the right SIM card. There is quite a big difference in what you get depending on the provider, so I thought I&amp;rsquo;d add my 2 cents.&lt;/p&gt;
&lt;h2 id=&#34;not-all-sims-are-equal&#34;&gt;Not all SIMs are equal&lt;/h2&gt;
&lt;p&gt;If we turn back the clock a couple years - the main carrier SIMs (docomo, softbank, AU) were &lt;strong&gt;expensive&lt;/strong&gt; and predatory. It was easy to get hooked into a 2-year contract costing around 8000 yen a month just for a good data connection. The best you could do was getting something like IIJMO (or a dozen other MVNO options) that would be on the docomo or AU wholesale network, but never be on-par with main-network speed and reliability, often lacking big features like Apple Watch support or 5G.&lt;/p&gt;
&lt;p&gt;MVNOs, or &amp;ldquo;Mobile Virtual Network Operators,&amp;rdquo; are virtual operators that don&amp;rsquo;t own their infrastructure or network. These companies, like IIJMO, Mineo, HIS Mobile, have agreements with main network operators (docomo, au, SoftBank, Rakuten) to resell part of their wholesale network, usually at cheaper rates but with varying quality.&lt;/p&gt;
&lt;p&gt;But all of that changed when Abe stepped down and Suga took over as prime minister. He made 3 important changes to how the big 3 had to operate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Providers can&amp;rsquo;t SIM-lock phones&lt;/strong&gt; - all phones you buy from providers have to have an option to unlock them at any time without paying a huge amount of money&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Providers can no longer charge a contract cancellation fee&lt;/strong&gt; to spark stronger competition - so switching from, say, docomo to AU is now free of charge&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The big providers must offer a cheap plan&lt;/strong&gt; on their main network that is not MVNOs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This changed the entire market: Carriers can no longer lock customers into contracts for years and need to actually offer something unique, otherwise customers will leave.&lt;/p&gt;
&lt;p&gt;The big 3 reacted by first offering cheaper main-network SIMs through their sub-brands (&lt;strong&gt;Y!Mobile&lt;/strong&gt; for Softbank, &lt;strong&gt;UQ Mobile&lt;/strong&gt; for AU). This was already great, because now we had plans that were owned by the big carriers themselves, but the government wasn&amp;rsquo;t happy about that, and told them again: you have to offer cheaper plans on your main brand!&lt;/p&gt;
&lt;p&gt;This gave us 3 new plans: &lt;strong&gt;Ahamo&lt;/strong&gt; from docomo, &lt;strong&gt;Povo&lt;/strong&gt; from AU and &lt;strong&gt;LINEMO&lt;/strong&gt; from SoftBank. The important point here is that all these plans, as I just mentioned, are owned by the big 3, so they are not MVNOs that buy wholesale and piggyback. You get the &lt;em&gt;same&lt;/em&gt; speed and network quality as &amp;ldquo;normal&amp;rdquo; customers of docomo/au/softbank, with no worse deal. These SIMs will almost always be better than any of the MVNOs available!&lt;/p&gt;
&lt;p&gt;So as a first line to choose from, my recommendations would be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On SoftBank: &lt;strong&gt;LINEMO&lt;/strong&gt;, &lt;strong&gt;Y!Mobile&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;On NTT Docomo: &lt;strong&gt;Ahamo&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;On KDDI/AU: &lt;strong&gt;Povo&lt;/strong&gt;, &lt;strong&gt;UQ Mobile&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;On Rakuten Mobile: &lt;strong&gt;Rakuten Mobile&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For today&amp;rsquo;s post I will be focusing on &lt;strong&gt;&lt;a href=&#34;https://ahamo.com&#34;&gt;Ahamo&lt;/a&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;a href=&#34;https://povo.jp&#34;&gt;Povo&lt;/a&gt;&lt;/strong&gt; (Promo code: &lt;code&gt;LCOD14FW&lt;/code&gt;) and &lt;strong&gt;&lt;a href=&#34;https://r10.to/hN5zpk&#34;&gt;Rakuten Mobile&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&#34;a-quick-look-at-the-networks&#34;&gt;A quick look at the networks&lt;/h2&gt;
&lt;p&gt;Ahamo runs on docomo, Povo on AU, and Rakuten Mobile on its own network.&lt;/p&gt;
&lt;p&gt;In general, &lt;strong&gt;docomo has the strongest network&lt;/strong&gt; with the fastest speed, &lt;strong&gt;followed by AU&lt;/strong&gt;. Rakuten is the new kid on the block, and while their network is steadily growing, it&amp;rsquo;s underwhelming for speed and coverage. There are always patches where one is better than the other, but in general, docomo takes the lead. At the time of writing, the AU network has the best coverage on 5G, with docomo lagging slightly behind.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rakuten Mobile&lt;/strong&gt; has a unique roaming agreement with AU, so in places where Rakuten Mobile isn&amp;rsquo;t available yet, it falls back to some offering from AU. This is NOT the same network as main AU or Povo. It is part of the wholesale network that is being rented out to MVNOs. I couldn&amp;rsquo;t test this as it happens automatically, but I often noticed that my AU line had reception while Rakuten Mobile did not.&lt;/p&gt;
&lt;p&gt;Rakuten Mobile often shows &amp;lsquo;5G&amp;rsquo; connection, but the speed and throughput is far from it, often comparable with lower 4G speed. Yes, the connection to the tower is 5G, but that doesn&amp;rsquo;t mean anything if you don&amp;rsquo;t get 5G benefits; it just eats more battery. This was the only provider where I actually felt that when I entered a URL, I often had to wait a few seconds before anything actually happened, especially in buses or trains.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bonus points:&lt;/strong&gt; Docomo/Ahamo allows you to purchase an extra option (currently costing 0 yen/month) that enables &lt;a href=&#34;https://www.docomo.ne.jp/service/5g_sa/&#34;&gt;5G SA (5G standalone)&lt;/a&gt; that moves you to a dedicated 5G core network, and gives you ridiculous speeds, currently claimed at &lt;em&gt;受信実効速度は170Mbps~484Mbpsです。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./img_02_pc.png&#34; alt=&#34;5G SA(Standalone)とはの画像&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Only docomo offers 5G SA currently, and the addon is still free for an unspecified amount of time. This is by far the best in terms of speed if that&amp;rsquo;s what you&amp;rsquo;re after.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;However,&lt;/strong&gt; while docomo has the best network, it&amp;rsquo;s also used by a lot of people. So in very crowded areas like at festivals or concerts, it&amp;rsquo;s usually the worst performing. Ironically, Rakuten is the best for those because it has the smallest customer base.&lt;/p&gt;
&lt;h2 id=&#34;data-volume-and-plans&#34;&gt;Data volume and plans&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Rakuten Mobile&lt;/strong&gt; has the most attractive deal here. It&amp;rsquo;s unlimited, and the price goes up depending on how much data you use, but is capped at 3000 yen for everything beyond 20GB.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./img-saikyo-price-data-pc.png&#34; alt=&#34;毎月のデータ利用量でお支払い金額が決まる 20GB超過後無制限※2で2,980円/月(税込3,278円)さらに使わなければ勝手に安くなる! ※2 公平なサービス提供のため速度制限する場合あり。環境により速度低下する場合あり ※通話料等別費用&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ahamo&lt;/strong&gt; has 2 options: 20gb for 3000 yen, or 100gb for 5000 yen. If you hit the upper limit you get speed limited to 1Mbps which is still pretty good.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./image-20231124142258682.png&#34; alt=&#34;image-20231124142258682&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Povo&lt;/strong&gt; has the most unique offering here: Toppings!&lt;/p&gt;
&lt;p&gt;By default, if you pay 0 yen a month, you get 128kbps. That&amp;rsquo;s very slow, but it&amp;rsquo;s free and unlimited. Though you won&amp;rsquo;t be able to do much outside of messaging with it. But did I mention it&amp;rsquo;s free?&lt;/p&gt;
&lt;p&gt;Then you can activate different toppings depending on how much data you need&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./IMG_0411-0803492.png&#34; alt=&#34;IMG_0411&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;As with the others, there is a 20GB plan for ~3000 yen, but more interesting options like unlimited for 24h for 330 yen, or 150 GB for 13,000 yen, valid for 6 months.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been using this SIM a lot on my iPad for its 0 yen option, and then when I have a day of heavy working ahead of me, I buy the unlimited topping and tether to my other devices.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Provider&lt;/th&gt;
          &lt;th&gt;3GB&lt;/th&gt;
          &lt;th&gt;20GB&lt;/th&gt;
          &lt;th&gt;100GB&lt;/th&gt;
          &lt;th&gt;Unlimited&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Rakuten Mobile&lt;/td&gt;
          &lt;td&gt;980 yen&lt;/td&gt;
          &lt;td&gt;2980 yen&lt;/td&gt;
          &lt;td&gt;2980 yen&lt;/td&gt;
          &lt;td&gt;2980 yen&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Povo&lt;/td&gt;
          &lt;td&gt;990 yen&lt;/td&gt;
          &lt;td&gt;2700 yen&lt;/td&gt;
          &lt;td&gt;4000 yen (50GB) * 2 or 7400 (200GB / 2)&lt;/td&gt;
          &lt;td&gt;330 * days&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Ahamo&lt;/td&gt;
          &lt;td&gt;2970 yen&lt;/td&gt;
          &lt;td&gt;2970 yen&lt;/td&gt;
          &lt;td&gt;4950 yen&lt;/td&gt;
          &lt;td&gt;-&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Considering this, Rakuten Mobile appears to be the most cost-effective as it never exceeds 2980 yen. However, its network quality is considerably lower. Using a plan like 150 GB for 6 months with Povo can drastically reduce the cost if you don&amp;rsquo;t use the full 20 GB each month anyway. It&amp;rsquo;s also a great sub-SIM because of the 0 yen base plan, and you can always add more data when you need it.&lt;/p&gt;
&lt;p&gt;Ahamo is the least complicated - either 20 GB or 100 GB. 20 GB was always enough for me, but having the option to upgrade to 100 GB when needed is very nice, and 1 Mbps is still not bad when it runs out.&lt;/p&gt;
&lt;h2 id=&#34;roaming-when-abroad&#34;&gt;Roaming when abroad&lt;/h2&gt;
&lt;p&gt;Roaming is often inconvenient and expensive. It&amp;rsquo;s usually better to just get a local SIM or eSIM through something like &lt;a href=&#34;https://ref.airalo.com/21Qr&#34;&gt;Airalo&lt;/a&gt; and be done with it, but with those new plans we may no longer need to do that.&lt;/p&gt;
&lt;p&gt;Ahamo surprised when it got announced that it comes with 20 GB of data roaming free of charge!! That&amp;rsquo;s honestly bonkers and means that in most countries, you can just leave the SIM active and use free data roaming without any additional steps.&lt;/p&gt;
&lt;p&gt;This offer is limited to 14 days of constant roaming. If you are longer than 14 days outside of Japan, the speed will get reduced to 128 kbps. You&amp;rsquo;ll still have unlimited data abroad for messaging, but yeah, it&amp;rsquo;s gonna be pretty slow. You also can&amp;rsquo;t purchase additional data when it runs out. You have to return to Japan for it to refresh.&lt;/p&gt;
&lt;p&gt;Next in line is Rakuten Mobile - it also gives you 128 kbps unlimited at slow speeds when abroad, or 2 GB at fast speeds free of additional charge (you can toggle fast/slow within the app). Additional volume is available at 1 GB for 500 yen. Still not bad, especially with unlimited at 128 kbps.&lt;/p&gt;
&lt;p&gt;Povo is sadly quite bad. They tried to continue with the topping model but all of the options available are pretty pricey - like 5 GB for 14 days for 7080 yen or 3 GB for 7 days for 4280 yen&amp;hellip; yeah don&amp;rsquo;t use this SIM abroad.&lt;/p&gt;
&lt;h2 id=&#34;other-special-perks-apple-watch-carrier-pay-etc&#34;&gt;Other special perks (Apple Watch, carrier pay, etc)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Ahamo&lt;/strong&gt; has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Carrier pay (pay through your phone bill)&lt;/li&gt;
&lt;li&gt;Apple Watch data at 500 yen/month&lt;/li&gt;
&lt;li&gt;5G SA (mentioned above)&lt;/li&gt;
&lt;li&gt;Free 20 GB international data roaming at no extra cost&lt;/li&gt;
&lt;li&gt;5 min of each phone call is free&lt;/li&gt;
&lt;li&gt;Free use of docomo wifi hotspots&lt;/li&gt;
&lt;li&gt;Synergy if you use d払い&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Povo&lt;/strong&gt; has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No Apple Watch :(&lt;/li&gt;
&lt;li&gt;Carrier Pay (AUかんたん決済)&lt;/li&gt;
&lt;li&gt;Unlimited data at 330 yen&lt;/li&gt;
&lt;li&gt;Unlimited data at 128 kbps at actual 0 yen base plan&lt;/li&gt;
&lt;li&gt;Free use of AU wifi hotspots, Wi2Premium and a few more&lt;/li&gt;
&lt;li&gt;Synergy if you use AU Pay&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Rakuten Mobile&lt;/strong&gt; has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Apple Watch data at 500 yen/month&lt;/li&gt;
&lt;li&gt;Free international data roaming for 2 GB, unlimited at 128 kbps&lt;/li&gt;
&lt;li&gt;Actual unlimited plan for cheap&lt;/li&gt;
&lt;li&gt;Carrier Pay&amp;hellip;? at some places (Google Play Store), but most don&amp;rsquo;t support it&lt;/li&gt;
&lt;li&gt;Ability to activate or re-issue an eSIM within ~2 minutes online&lt;/li&gt;
&lt;li&gt;Free phone calls, SMS through their apps&lt;/li&gt;
&lt;li&gt;Gives you extra points on Rakuten Ichiba, synergy with other Rakuten products like Rakuten Card&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;*Rakuten Mobile offers &amp;ldquo;free phone calls and free SMS&amp;rdquo; but it&amp;rsquo;s the weirdest thing. SMS only works on Android if you use their Rakuten Link app, which for whatever reason doesn&amp;rsquo;t offer free SMS on iPhone. Free calls is also only if you use the Rakuten Link app, so it&amp;rsquo;s VOIP and not true cellular - it&amp;rsquo;s often very janky.&lt;/p&gt;
&lt;p&gt;Rakuten Mobile also just stopped working in the past when a new iPhone got released, and some Android phones aren&amp;rsquo;t even supported for whatever reason (have you ever seen a physical SIM not supported on a phone??), so it&amp;rsquo;s by far the buggiest of the providers. They also constantly try to upsell your some other Rakuten product, even their Rakuten Link app is plastered with ads&amp;hellip;&lt;/p&gt;
&lt;h2 id=&#34;so-david-which-one&#34;&gt;So David, which one?&lt;/h2&gt;
&lt;p&gt;In the end it depends on what you want, no? But if you&amp;rsquo;re still struggling with deciding which one fits the best, my recommendation would be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ahamo&lt;/strong&gt; if you just want 20 GB at fast speed, have an Apple Watch and/or travel a lot. The 20 GB roaming is unbeatable, so is 5G SA. 100 GB for 5000 yen is still reasonable for when you need it. The best all-around SIM.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Povo&lt;/strong&gt; for their unique topping mechanism and still &lt;em&gt;very&lt;/em&gt; good network, even better than docomo in some areas and crowded places, or with 5G coverage. The 0 yen base plan makes this a no-brainer to have &amp;lsquo;just in case&amp;rsquo;, or put onto an iPad to always have a 128 kbps data connection on hand. 330 yen for unlimited when needed is damn good for remote working. No Apple Watch option though, and their roaming offering sucks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rakuten Mobile&lt;/strong&gt; if you want something unlimited and don&amp;rsquo;t care much about network, speed, throughput or coverage. It&amp;rsquo;s good enough in cities and a great secondary SIM to have for when Docomo or AU are overloaded, or when travelling. The ability to quickly move the eSIM to another device has been a life-saver. But, as you can tell from the post, the network is the most &amp;lsquo;meh&amp;rsquo;, has high latency, lots of dark spots, and other weirdness like their Rakuten Link app having different behavior on iPhone or Android. Also I &lt;em&gt;hate&lt;/em&gt; that they constantly try to upsell me when I&amp;rsquo;m already a customer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the end, just try them and see which one you like the most. There is no contract cancellation fee anymore, so it&amp;rsquo;s very easy to move a number from plan A to plan B and back.&lt;/p&gt;
&lt;p&gt;If you liked this post, please us my referral codes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Povo: &lt;code&gt;LCOD14FW&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Rakuten Mobile: &lt;a href=&#34;https://r10.to/hN5zpk&#34;&gt;https://r10.to/hN5zpk&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Airalo: &lt;a href=&#34;https://ref.airalo.com/21Qr&#34;&gt;https://ref.airalo.com/21Qr&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/japanese-sim-cards/banner.png" /></p><p>I was discussing Japanese SIM cards with a friend the other day and noticed that there aren&rsquo;t really any English resources to help people choose the right SIM card. There is quite a big difference in what you get depending on the provider, so I thought I&rsquo;d add my 2 cents.</p>
<h2 id="not-all-sims-are-equal">Not all SIMs are equal</h2>
<p>If we turn back the clock a couple years - the main carrier SIMs (docomo, softbank, AU) were <strong>expensive</strong> and predatory. It was easy to get hooked into a 2-year contract costing around 8000 yen a month just for a good data connection. The best you could do was getting something like IIJMO (or a dozen other MVNO options) that would be on the docomo or AU wholesale network, but never be on-par with main-network speed and reliability, often lacking big features like Apple Watch support or 5G.</p>
<p>MVNOs, or &ldquo;Mobile Virtual Network Operators,&rdquo; are virtual operators that don&rsquo;t own their infrastructure or network. These companies, like IIJMO, Mineo, HIS Mobile, have agreements with main network operators (docomo, au, SoftBank, Rakuten) to resell part of their wholesale network, usually at cheaper rates but with varying quality.</p>
<p>But all of that changed when Abe stepped down and Suga took over as prime minister. He made 3 important changes to how the big 3 had to operate:</p>
<ul>
<li><strong>Providers can&rsquo;t SIM-lock phones</strong> - all phones you buy from providers have to have an option to unlock them at any time without paying a huge amount of money</li>
<li><strong>Providers can no longer charge a contract cancellation fee</strong> to spark stronger competition - so switching from, say, docomo to AU is now free of charge</li>
<li><strong>The big providers must offer a cheap plan</strong> on their main network that is not MVNOs</li>
</ul>
<p>This changed the entire market: Carriers can no longer lock customers into contracts for years and need to actually offer something unique, otherwise customers will leave.</p>
<p>The big 3 reacted by first offering cheaper main-network SIMs through their sub-brands (<strong>Y!Mobile</strong> for Softbank, <strong>UQ Mobile</strong> for AU). This was already great, because now we had plans that were owned by the big carriers themselves, but the government wasn&rsquo;t happy about that, and told them again: you have to offer cheaper plans on your main brand!</p>
<p>This gave us 3 new plans: <strong>Ahamo</strong> from docomo, <strong>Povo</strong> from AU and <strong>LINEMO</strong> from SoftBank. The important point here is that all these plans, as I just mentioned, are owned by the big 3, so they are not MVNOs that buy wholesale and piggyback. You get the <em>same</em> speed and network quality as &ldquo;normal&rdquo; customers of docomo/au/softbank, with no worse deal. These SIMs will almost always be better than any of the MVNOs available!</p>
<p>So as a first line to choose from, my recommendations would be:</p>
<ul>
<li>On SoftBank: <strong>LINEMO</strong>, <strong>Y!Mobile</strong></li>
<li>On NTT Docomo: <strong>Ahamo</strong></li>
<li>On KDDI/AU: <strong>Povo</strong>, <strong>UQ Mobile</strong></li>
<li>On Rakuten Mobile: <strong>Rakuten Mobile</strong></li>
</ul>
<p>For today&rsquo;s post I will be focusing on <strong><a href="https://ahamo.com">Ahamo</a></strong>, <strong><a href="https://povo.jp">Povo</a></strong> (Promo code: <code>LCOD14FW</code>) and <strong><a href="https://r10.to/hN5zpk">Rakuten Mobile</a></strong>.</p>
<h2 id="a-quick-look-at-the-networks">A quick look at the networks</h2>
<p>Ahamo runs on docomo, Povo on AU, and Rakuten Mobile on its own network.</p>
<p>In general, <strong>docomo has the strongest network</strong> with the fastest speed, <strong>followed by AU</strong>. Rakuten is the new kid on the block, and while their network is steadily growing, it&rsquo;s underwhelming for speed and coverage. There are always patches where one is better than the other, but in general, docomo takes the lead. At the time of writing, the AU network has the best coverage on 5G, with docomo lagging slightly behind.</p>
<p><strong>Rakuten Mobile</strong> has a unique roaming agreement with AU, so in places where Rakuten Mobile isn&rsquo;t available yet, it falls back to some offering from AU. This is NOT the same network as main AU or Povo. It is part of the wholesale network that is being rented out to MVNOs. I couldn&rsquo;t test this as it happens automatically, but I often noticed that my AU line had reception while Rakuten Mobile did not.</p>
<p>Rakuten Mobile often shows &lsquo;5G&rsquo; connection, but the speed and throughput is far from it, often comparable with lower 4G speed. Yes, the connection to the tower is 5G, but that doesn&rsquo;t mean anything if you don&rsquo;t get 5G benefits; it just eats more battery. This was the only provider where I actually felt that when I entered a URL, I often had to wait a few seconds before anything actually happened, especially in buses or trains.</p>
<p><strong>Bonus points:</strong> Docomo/Ahamo allows you to purchase an extra option (currently costing 0 yen/month) that enables <a href="https://www.docomo.ne.jp/service/5g_sa/">5G SA (5G standalone)</a> that moves you to a dedicated 5G core network, and gives you ridiculous speeds, currently claimed at <em>受信実効速度は170Mbps~484Mbpsです。</em></p>
<p>
<img class="img-fluid" src="./img_02_pc.png" alt="5G SA(Standalone)とはの画像" />

</p>
<p>Only docomo offers 5G SA currently, and the addon is still free for an unspecified amount of time. This is by far the best in terms of speed if that&rsquo;s what you&rsquo;re after.</p>
<p><strong>However,</strong> while docomo has the best network, it&rsquo;s also used by a lot of people. So in very crowded areas like at festivals or concerts, it&rsquo;s usually the worst performing. Ironically, Rakuten is the best for those because it has the smallest customer base.</p>
<h2 id="data-volume-and-plans">Data volume and plans</h2>
<p><strong>Rakuten Mobile</strong> has the most attractive deal here. It&rsquo;s unlimited, and the price goes up depending on how much data you use, but is capped at 3000 yen for everything beyond 20GB.</p>
<p>
<img class="img-fluid" src="./img-saikyo-price-data-pc.png" alt="毎月のデータ利用量でお支払い金額が決まる 20GB超過後無制限※2で2,980円/月(税込3,278円)さらに使わなければ勝手に安くなる! ※2 公平なサービス提供のため速度制限する場合あり。環境により速度低下する場合あり ※通話料等別費用" />

</p>
<p><strong>Ahamo</strong> has 2 options: 20gb for 3000 yen, or 100gb for 5000 yen. If you hit the upper limit you get speed limited to 1Mbps which is still pretty good.</p>
<p>
<img class="img-fluid" src="./image-20231124142258682.png" alt="image-20231124142258682" />

</p>
<p><strong>Povo</strong> has the most unique offering here: Toppings!</p>
<p>By default, if you pay 0 yen a month, you get 128kbps. That&rsquo;s very slow, but it&rsquo;s free and unlimited. Though you won&rsquo;t be able to do much outside of messaging with it. But did I mention it&rsquo;s free?</p>
<p>Then you can activate different toppings depending on how much data you need</p>
<p>
<img class="img-fluid" src="./IMG_0411-0803492.png" alt="IMG_0411" />

</p>
<p>As with the others, there is a 20GB plan for ~3000 yen, but more interesting options like unlimited for 24h for 330 yen, or 150 GB for 13,000 yen, valid for 6 months.</p>
<p>I&rsquo;ve been using this SIM a lot on my iPad for its 0 yen option, and then when I have a day of heavy working ahead of me, I buy the unlimited topping and tether to my other devices.</p>
<table>
  <thead>
      <tr>
          <th>Provider</th>
          <th>3GB</th>
          <th>20GB</th>
          <th>100GB</th>
          <th>Unlimited</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Rakuten Mobile</td>
          <td>980 yen</td>
          <td>2980 yen</td>
          <td>2980 yen</td>
          <td>2980 yen</td>
      </tr>
      <tr>
          <td>Povo</td>
          <td>990 yen</td>
          <td>2700 yen</td>
          <td>4000 yen (50GB) * 2 or 7400 (200GB / 2)</td>
          <td>330 * days</td>
      </tr>
      <tr>
          <td>Ahamo</td>
          <td>2970 yen</td>
          <td>2970 yen</td>
          <td>4950 yen</td>
          <td>-</td>
      </tr>
  </tbody>
</table>
<p>Considering this, Rakuten Mobile appears to be the most cost-effective as it never exceeds 2980 yen. However, its network quality is considerably lower. Using a plan like 150 GB for 6 months with Povo can drastically reduce the cost if you don&rsquo;t use the full 20 GB each month anyway. It&rsquo;s also a great sub-SIM because of the 0 yen base plan, and you can always add more data when you need it.</p>
<p>Ahamo is the least complicated - either 20 GB or 100 GB. 20 GB was always enough for me, but having the option to upgrade to 100 GB when needed is very nice, and 1 Mbps is still not bad when it runs out.</p>
<h2 id="roaming-when-abroad">Roaming when abroad</h2>
<p>Roaming is often inconvenient and expensive. It&rsquo;s usually better to just get a local SIM or eSIM through something like <a href="https://ref.airalo.com/21Qr">Airalo</a> and be done with it, but with those new plans we may no longer need to do that.</p>
<p>Ahamo surprised when it got announced that it comes with 20 GB of data roaming free of charge!! That&rsquo;s honestly bonkers and means that in most countries, you can just leave the SIM active and use free data roaming without any additional steps.</p>
<p>This offer is limited to 14 days of constant roaming. If you are longer than 14 days outside of Japan, the speed will get reduced to 128 kbps. You&rsquo;ll still have unlimited data abroad for messaging, but yeah, it&rsquo;s gonna be pretty slow. You also can&rsquo;t purchase additional data when it runs out. You have to return to Japan for it to refresh.</p>
<p>Next in line is Rakuten Mobile - it also gives you 128 kbps unlimited at slow speeds when abroad, or 2 GB at fast speeds free of additional charge (you can toggle fast/slow within the app). Additional volume is available at 1 GB for 500 yen. Still not bad, especially with unlimited at 128 kbps.</p>
<p>Povo is sadly quite bad. They tried to continue with the topping model but all of the options available are pretty pricey - like 5 GB for 14 days for 7080 yen or 3 GB for 7 days for 4280 yen&hellip; yeah don&rsquo;t use this SIM abroad.</p>
<h2 id="other-special-perks-apple-watch-carrier-pay-etc">Other special perks (Apple Watch, carrier pay, etc)</h2>
<p><strong>Ahamo</strong> has:</p>
<ul>
<li>Carrier pay (pay through your phone bill)</li>
<li>Apple Watch data at 500 yen/month</li>
<li>5G SA (mentioned above)</li>
<li>Free 20 GB international data roaming at no extra cost</li>
<li>5 min of each phone call is free</li>
<li>Free use of docomo wifi hotspots</li>
<li>Synergy if you use d払い</li>
</ul>
<p><strong>Povo</strong> has:</p>
<ul>
<li>No Apple Watch :(</li>
<li>Carrier Pay (AUかんたん決済)</li>
<li>Unlimited data at 330 yen</li>
<li>Unlimited data at 128 kbps at actual 0 yen base plan</li>
<li>Free use of AU wifi hotspots, Wi2Premium and a few more</li>
<li>Synergy if you use AU Pay</li>
</ul>
<p><strong>Rakuten Mobile</strong> has:</p>
<ul>
<li>Apple Watch data at 500 yen/month</li>
<li>Free international data roaming for 2 GB, unlimited at 128 kbps</li>
<li>Actual unlimited plan for cheap</li>
<li>Carrier Pay&hellip;? at some places (Google Play Store), but most don&rsquo;t support it</li>
<li>Ability to activate or re-issue an eSIM within ~2 minutes online</li>
<li>Free phone calls, SMS through their apps</li>
<li>Gives you extra points on Rakuten Ichiba, synergy with other Rakuten products like Rakuten Card</li>
</ul>
<p>*Rakuten Mobile offers &ldquo;free phone calls and free SMS&rdquo; but it&rsquo;s the weirdest thing. SMS only works on Android if you use their Rakuten Link app, which for whatever reason doesn&rsquo;t offer free SMS on iPhone. Free calls is also only if you use the Rakuten Link app, so it&rsquo;s VOIP and not true cellular - it&rsquo;s often very janky.</p>
<p>Rakuten Mobile also just stopped working in the past when a new iPhone got released, and some Android phones aren&rsquo;t even supported for whatever reason (have you ever seen a physical SIM not supported on a phone??), so it&rsquo;s by far the buggiest of the providers. They also constantly try to upsell your some other Rakuten product, even their Rakuten Link app is plastered with ads&hellip;</p>
<h2 id="so-david-which-one">So David, which one?</h2>
<p>In the end it depends on what you want, no? But if you&rsquo;re still struggling with deciding which one fits the best, my recommendation would be:</p>
<ul>
<li><strong>Ahamo</strong> if you just want 20 GB at fast speed, have an Apple Watch and/or travel a lot. The 20 GB roaming is unbeatable, so is 5G SA. 100 GB for 5000 yen is still reasonable for when you need it. The best all-around SIM.</li>
<li><strong>Povo</strong> for their unique topping mechanism and still <em>very</em> good network, even better than docomo in some areas and crowded places, or with 5G coverage. The 0 yen base plan makes this a no-brainer to have &lsquo;just in case&rsquo;, or put onto an iPad to always have a 128 kbps data connection on hand. 330 yen for unlimited when needed is damn good for remote working. No Apple Watch option though, and their roaming offering sucks.</li>
<li><strong>Rakuten Mobile</strong> if you want something unlimited and don&rsquo;t care much about network, speed, throughput or coverage. It&rsquo;s good enough in cities and a great secondary SIM to have for when Docomo or AU are overloaded, or when travelling. The ability to quickly move the eSIM to another device has been a life-saver. But, as you can tell from the post, the network is the most &lsquo;meh&rsquo;, has high latency, lots of dark spots, and other weirdness like their Rakuten Link app having different behavior on iPhone or Android. Also I <em>hate</em> that they constantly try to upsell me when I&rsquo;m already a customer.</li>
</ul>
<p>In the end, just try them and see which one you like the most. There is no contract cancellation fee anymore, so it&rsquo;s very easy to move a number from plan A to plan B and back.</p>
<p>If you liked this post, please us my referral codes:</p>
<ul>
<li>Povo: <code>LCOD14FW</code></li>
<li>Rakuten Mobile: <a href="https://r10.to/hN5zpk">https://r10.to/hN5zpk</a></li>
<li>Airalo: <a href="https://ref.airalo.com/21Qr">https://ref.airalo.com/21Qr</a></li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>Maskwire - Building the email service I always wanted</title>
      <link>https://david.coffee/maskwire/</link>
      <pubDate>Tue, 25 Jul 2023 10:00:00 +0900</pubDate>
      
      <guid>https://david.coffee/maskwire/</guid><media:content url="https://david.coffee/maskwire/logo-0357415.png" medium="image" />
          <enclosure url="https://david.coffee/maskwire/logo-0357415.png" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/maskwire/logo-0357415.png&#34; /&gt;&lt;/p&gt;&lt;p&gt;I am very happy to finally be able to talk a little about the pet project I&amp;rsquo;ve been hacking on for the past months while backpacking through Australia - &lt;a href=&#34;https://maskwire.com&#34;&gt;https://maskwire.com&lt;/a&gt;, and the email service that I always wanted but didn&amp;rsquo;t exist.&lt;/p&gt;
&lt;h2 id=&#34;how-im-using-email-today&#34;&gt;How I&amp;rsquo;m using email today&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m not going to talk about reinventing email into some chat messenger or something like that. On the contrary - I think email is great the way it is and I like using it.&lt;/p&gt;
&lt;p&gt;But because of spam and usage of my own domain, I developed a pretty efficient strategy to combat spam, by using one-time-use email addresses. In the beginning, I just used stuff like &amp;ldquo;&lt;a href=&#34;mailto:facebook@davemail.io&#34;&gt;facebook@davemail.io&lt;/a&gt;&amp;rdquo; where *@davemail.io is a wildcard email address that resolves to my main email. Worked great!&lt;/p&gt;
&lt;p&gt;I liked this so much that I built my own service that would generate one-time-use emails, but as things go - time to market took way too long and by the time I was ready to launch there were already heaps of other services from Mozilla and more notably, Apple.&lt;/p&gt;
&lt;p&gt;As the next iteration, I switched to Fastmail&amp;rsquo;s &amp;ldquo;Masked Email&amp;rdquo; feature, which now spits out a random email address whenever I need one. I liked this system enough that I built a bunch of tools around it: &lt;a href=&#34;https://maskedemailmanager.david.coffee&#34;&gt;An iOS app&lt;/a&gt;, &lt;a href=&#34;https://github.com/dvcrn/maskedemail-cli&#34;&gt;a CLI&lt;/a&gt;, a &lt;a href=&#34;https://github.com/dvcrn/maskedemail-cli&#34;&gt;WASM based NPM package&lt;/a&gt; among others. So far so good.&lt;/p&gt;
&lt;p&gt;I now have a barrage of different domains that are all managed through Cloudflare and Fastmail.&lt;/p&gt;
&lt;h3 id=&#34;what-i-am-not-happy-with&#34;&gt;What I am not happy with&lt;/h3&gt;
&lt;p&gt;In short - email clients. Each client has different capabilities and different quirks, and don&amp;rsquo;t even get me started on support for wildcard/catch-all emails. It&amp;rsquo;s garbage.&lt;/p&gt;
&lt;p&gt;Ever received an email on &lt;a href=&#34;mailto:facebook@davemail.io&#34;&gt;facebook@davemail.io&lt;/a&gt; and want to reply from it? Yeah, you can&amp;rsquo;t. You gotta open the web interface and if you&amp;rsquo;re lucky, the service you use supports replying from it (Fastmail does, sometimes). Otherwise - open your client settings, add a new alias for &lt;a href=&#34;mailto:facebook@davemail.io&#34;&gt;facebook@davemail.io&lt;/a&gt;, and only then can you reply from it.&lt;/p&gt;
&lt;p&gt;Swipe actions are another one of my gripes. Sometimes swiping an email in a client like Apple Mail deletes it, sometimes it archives it. Sometimes you can configure it, sometimes you can&amp;rsquo;t. In Gmail or Samsung Mail you can&amp;rsquo;t; emails always go to trash. Great.&lt;/p&gt;
&lt;p&gt;What if I click on a &amp;ldquo;contact developer&amp;rdquo; link in an app? Apple Mail will open with a compose window that has my main email address selected with no option to generate a new custom address just for that case (unless you use Apple&amp;rsquo;s Hide My Email feature).&lt;/p&gt;
&lt;p&gt;Apple Mail, while great, also has the quirk of downloading &lt;em&gt;everything&lt;/em&gt; on an IMAP server, so in my case about 50,000 accumulated emails - whoops.&lt;/p&gt;
&lt;p&gt;There is so much inconsistency and quirks, it&amp;rsquo;s just not very productive.&lt;/p&gt;
&lt;h2 id=&#34;building-the-email-service-i-always-wanted&#34;&gt;Building the email service I always wanted&lt;/h2&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./maskwire-ss1.png&#34; alt=&#34;Screenshot 2023-07-16 at 18.57.46&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Step by step I fixed issues like those mentioned above for myself, but if I have these issues, others may too. So the idea slowly transformed from &amp;ldquo;a service that generates one-time-use emails&amp;rdquo; to a service that works on every device and instead enables advanced (sometimes niche) functionality that email clients just can&amp;rsquo;t provide.&lt;/p&gt;
&lt;p&gt;Maskwire works as a transparent IMAP/SMTP server acting in the middle, communicating 1:1 with an upstream server (relaying client commands directly to the provider), but contains a custom message rewriting and remapping engine.&lt;/p&gt;
&lt;p&gt;I want to reply from &lt;a href=&#34;mailto:facebook@davemail.io&#34;&gt;facebook@davemail.io&lt;/a&gt;? Now I just reply to the email that I received on it in Apple Mail without thinking about it and let Maskwire handle sending that email out from &lt;a href=&#34;mailto:facebook@davemail.io&#34;&gt;facebook@davemail.io&lt;/a&gt; again.&lt;/p&gt;
&lt;p&gt;I keep deleting emails accidentally by swiping them? Well that&amp;rsquo;s fixed now too, Maskwire can just rewrite the &amp;ldquo;move to Trash&amp;rdquo; command into a &amp;ldquo;move to Archive&amp;rdquo; command.&lt;/p&gt;
&lt;p&gt;I click on a &amp;ldquo;contact developer&amp;rdquo; link and Apple Mail comes up with a compose dialog? I don&amp;rsquo;t need to care about selecting the correct sender, Maskwire will just rewrite it from &lt;a href=&#34;mailto:my@mail.com&#34;&gt;my@mail.com&lt;/a&gt; to &lt;a href=&#34;mailto:mail202307@davemail.io&#34;&gt;mail202307@davemail.io&lt;/a&gt; when the email gets sent out.&lt;/p&gt;
&lt;p&gt;Email inbox getting too big? Also no problem, I set up a filter in Maskwire and now only the last 1 month of messages in my archive is getting synchronized to the client, independent of their settings.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./maskwire-ss2.png&#34; alt=&#34;Maskwire Toolbox&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been chipping away on Maskwire and using it on my actual emails for the past weeks while fixing quirks and expanding functionality and I&amp;rsquo;m very happy that it&amp;rsquo;s now in a state where I can more openly talk about it and ask people to try it.&lt;/p&gt;
&lt;h3 id=&#34;what-about-security-doesnt-this-sound-like-a-recipe-for-disaster-and-hacking&#34;&gt;What about security? Doesn&amp;rsquo;t this sound like a recipe for disaster and hacking?&lt;/h3&gt;
&lt;p&gt;I went through different iterations of different security designs and landed on one that has been the most bulletproof. You still need to provide IMAP/SMTP credentials of your upstream provider (in my case Fastmail) to Maskwire, but upon account creation, Maskwire generates an &lt;strong&gt;RSA2048 keypair that is encrypted with the user&amp;rsquo;s Maskwire.com password&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The public key is then used to encrypt the upstream IMAP/SMTP credentials, which means that &lt;strong&gt;Maskwire literally has no way to decrypt those without the password of the private key, which it doesn&amp;rsquo;t possess&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Instead, when the user authenticates with the Maskwire IMAP/SMTP server, the password used for authenticating will be used to decrypt the private key, which will then decrypt upstream credentials to establish a session, then wiped from memory. Once the user logs out, the system is locked again.&lt;/p&gt;
&lt;p&gt;If the user doesn&amp;rsquo;t login with their email client, Maskwire can&amp;rsquo;t login either.&lt;/p&gt;
&lt;h2 id=&#34;maskwire-is-open-for-early-beta-now&#34;&gt;Maskwire is open for early beta now&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been using it for a while on my own emails and it&amp;rsquo;s working great for my use case, but as the only user I&amp;rsquo;m sure there will be quite a few bugs and things that won&amp;rsquo;t work yet.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d like to open signups to a broader audience and am looking for users willing to try something new. You can sign up at &lt;a href=&#34;https://maskwire.com&#34;&gt;https://maskwire.com&lt;/a&gt; - what you will need are IMAP/SMTP credentials and an email provider that you&amp;rsquo;re using with your own domains.&lt;/p&gt;
&lt;p&gt;Please also join the Maskwire Discord, so we can discuss features, bugs and more: &lt;a href=&#34;https://discord.gg/8QkduyQf8V&#34;&gt;https://discord.gg/8QkduyQf8V&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./logo-reounded-2.png&#34; alt=&#34;logo-reounded-2&#34; /&gt;

&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/maskwire/logo-0357415.png" /></p><p>I am very happy to finally be able to talk a little about the pet project I&rsquo;ve been hacking on for the past months while backpacking through Australia - <a href="https://maskwire.com">https://maskwire.com</a>, and the email service that I always wanted but didn&rsquo;t exist.</p>
<h2 id="how-im-using-email-today">How I&rsquo;m using email today</h2>
<p>I&rsquo;m not going to talk about reinventing email into some chat messenger or something like that. On the contrary - I think email is great the way it is and I like using it.</p>
<p>But because of spam and usage of my own domain, I developed a pretty efficient strategy to combat spam, by using one-time-use email addresses. In the beginning, I just used stuff like &ldquo;<a href="mailto:facebook@davemail.io">facebook@davemail.io</a>&rdquo; where *@davemail.io is a wildcard email address that resolves to my main email. Worked great!</p>
<p>I liked this so much that I built my own service that would generate one-time-use emails, but as things go - time to market took way too long and by the time I was ready to launch there were already heaps of other services from Mozilla and more notably, Apple.</p>
<p>As the next iteration, I switched to Fastmail&rsquo;s &ldquo;Masked Email&rdquo; feature, which now spits out a random email address whenever I need one. I liked this system enough that I built a bunch of tools around it: <a href="https://maskedemailmanager.david.coffee">An iOS app</a>, <a href="https://github.com/dvcrn/maskedemail-cli">a CLI</a>, a <a href="https://github.com/dvcrn/maskedemail-cli">WASM based NPM package</a> among others. So far so good.</p>
<p>I now have a barrage of different domains that are all managed through Cloudflare and Fastmail.</p>
<h3 id="what-i-am-not-happy-with">What I am not happy with</h3>
<p>In short - email clients. Each client has different capabilities and different quirks, and don&rsquo;t even get me started on support for wildcard/catch-all emails. It&rsquo;s garbage.</p>
<p>Ever received an email on <a href="mailto:facebook@davemail.io">facebook@davemail.io</a> and want to reply from it? Yeah, you can&rsquo;t. You gotta open the web interface and if you&rsquo;re lucky, the service you use supports replying from it (Fastmail does, sometimes). Otherwise - open your client settings, add a new alias for <a href="mailto:facebook@davemail.io">facebook@davemail.io</a>, and only then can you reply from it.</p>
<p>Swipe actions are another one of my gripes. Sometimes swiping an email in a client like Apple Mail deletes it, sometimes it archives it. Sometimes you can configure it, sometimes you can&rsquo;t. In Gmail or Samsung Mail you can&rsquo;t; emails always go to trash. Great.</p>
<p>What if I click on a &ldquo;contact developer&rdquo; link in an app? Apple Mail will open with a compose window that has my main email address selected with no option to generate a new custom address just for that case (unless you use Apple&rsquo;s Hide My Email feature).</p>
<p>Apple Mail, while great, also has the quirk of downloading <em>everything</em> on an IMAP server, so in my case about 50,000 accumulated emails - whoops.</p>
<p>There is so much inconsistency and quirks, it&rsquo;s just not very productive.</p>
<h2 id="building-the-email-service-i-always-wanted">Building the email service I always wanted</h2>
<p>
<img class="img-fluid" src="./maskwire-ss1.png" alt="Screenshot 2023-07-16 at 18.57.46" />

</p>
<p>Step by step I fixed issues like those mentioned above for myself, but if I have these issues, others may too. So the idea slowly transformed from &ldquo;a service that generates one-time-use emails&rdquo; to a service that works on every device and instead enables advanced (sometimes niche) functionality that email clients just can&rsquo;t provide.</p>
<p>Maskwire works as a transparent IMAP/SMTP server acting in the middle, communicating 1:1 with an upstream server (relaying client commands directly to the provider), but contains a custom message rewriting and remapping engine.</p>
<p>I want to reply from <a href="mailto:facebook@davemail.io">facebook@davemail.io</a>? Now I just reply to the email that I received on it in Apple Mail without thinking about it and let Maskwire handle sending that email out from <a href="mailto:facebook@davemail.io">facebook@davemail.io</a> again.</p>
<p>I keep deleting emails accidentally by swiping them? Well that&rsquo;s fixed now too, Maskwire can just rewrite the &ldquo;move to Trash&rdquo; command into a &ldquo;move to Archive&rdquo; command.</p>
<p>I click on a &ldquo;contact developer&rdquo; link and Apple Mail comes up with a compose dialog? I don&rsquo;t need to care about selecting the correct sender, Maskwire will just rewrite it from <a href="mailto:my@mail.com">my@mail.com</a> to <a href="mailto:mail202307@davemail.io">mail202307@davemail.io</a> when the email gets sent out.</p>
<p>Email inbox getting too big? Also no problem, I set up a filter in Maskwire and now only the last 1 month of messages in my archive is getting synchronized to the client, independent of their settings.</p>
<p>
<img class="img-fluid" src="./maskwire-ss2.png" alt="Maskwire Toolbox" />

</p>
<p>I&rsquo;ve been chipping away on Maskwire and using it on my actual emails for the past weeks while fixing quirks and expanding functionality and I&rsquo;m very happy that it&rsquo;s now in a state where I can more openly talk about it and ask people to try it.</p>
<h3 id="what-about-security-doesnt-this-sound-like-a-recipe-for-disaster-and-hacking">What about security? Doesn&rsquo;t this sound like a recipe for disaster and hacking?</h3>
<p>I went through different iterations of different security designs and landed on one that has been the most bulletproof. You still need to provide IMAP/SMTP credentials of your upstream provider (in my case Fastmail) to Maskwire, but upon account creation, Maskwire generates an <strong>RSA2048 keypair that is encrypted with the user&rsquo;s Maskwire.com password</strong>.</p>
<p>The public key is then used to encrypt the upstream IMAP/SMTP credentials, which means that <strong>Maskwire literally has no way to decrypt those without the password of the private key, which it doesn&rsquo;t possess</strong>.</p>
<p>Instead, when the user authenticates with the Maskwire IMAP/SMTP server, the password used for authenticating will be used to decrypt the private key, which will then decrypt upstream credentials to establish a session, then wiped from memory. Once the user logs out, the system is locked again.</p>
<p>If the user doesn&rsquo;t login with their email client, Maskwire can&rsquo;t login either.</p>
<h2 id="maskwire-is-open-for-early-beta-now">Maskwire is open for early beta now</h2>
<p>I&rsquo;ve been using it for a while on my own emails and it&rsquo;s working great for my use case, but as the only user I&rsquo;m sure there will be quite a few bugs and things that won&rsquo;t work yet.</p>
<p>I&rsquo;d like to open signups to a broader audience and am looking for users willing to try something new. You can sign up at <a href="https://maskwire.com">https://maskwire.com</a> - what you will need are IMAP/SMTP credentials and an email provider that you&rsquo;re using with your own domains.</p>
<p>Please also join the Maskwire Discord, so we can discuss features, bugs and more: <a href="https://discord.gg/8QkduyQf8V">https://discord.gg/8QkduyQf8V</a></p>
<p>
<img class="img-fluid" src="./logo-reounded-2.png" alt="logo-reounded-2" />

</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>The case for a travel router, feat. Beryl AX</title>
      <link>https://david.coffee/the-case-for-a-travel-router/</link>
      <pubDate>Sat, 22 Apr 2023 15:00:00 +0900</pubDate>
      
      <guid>https://david.coffee/the-case-for-a-travel-router/</guid><media:content url="https://david.coffee/teaser.jpg" medium="image" />
          <enclosure url="https://david.coffee/teaser.jpg" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/teaser.jpg&#34; /&gt;&lt;/p&gt;&lt;p&gt;Travel routers are a thing I didn&amp;rsquo;t know existed. It makes sense that they do when you think about it, but I was never in a situation where one might have been necessary. Recently, I started a months-long sabbatical in Australia, and part of my research of what to pack on subreddits like /r/onebag landed me on travel routers!&lt;/p&gt;
&lt;p&gt;After a bit of research, I went out and ordered the GL-Inet Beryl AX, a small cute mint-colored box that would fit into my pocket if I wanted to.&lt;/p&gt;
&lt;h2 id=&#34;what-are-travel-routers-and-why-i-wanted-one&#34;&gt;What are travel routers, and why I wanted one&lt;/h2&gt;
&lt;p&gt;As the name implied - routers for traveling (duh). They are usually very small, and feature WiFi and an ethernet port. More advanced models have more functionality like VPN, Wireguard tunnel, and whatnot, depending on your needs.&lt;/p&gt;
&lt;p&gt;The appeal for me is bringing my own network with me. If I hop between hotels, Airbnbs, and cafes, I don&amp;rsquo;t have to worry about setting up all my devices to use that network again and again - all I have to set up is the travel router and the rest &lt;em&gt;just works&lt;/em&gt;. The router can also add another layer of security with things like DNS-over-HTTPS, OpenVPN connections, or tailscale Wireguard tunnel on the router level, so none of my end devices have to know about any of that. I have one less thing I have to trust (to some extent).&lt;/p&gt;
&lt;p&gt;Another, maybe more silly reason was mDNS and Chromecast: One of the devices I wanted to bring with me was my small Chromecast because of its size, but those devices usually just won&amp;rsquo;t work with Hotel WiFis because they disable mDNS (not to mention the PITA to keep setting it up with new Airbnb wifis). If I have my own pocket WiFi with me, however, I can use the Chromecast just fine, for the occasional movie session.&lt;/p&gt;
&lt;p&gt;All in all, a travel router I can pocket sounded like a good idea.&lt;/p&gt;
&lt;h2 id=&#34;the-beryl-ax-and-what-it-can-do&#34;&gt;The Beryl AX, and what it can do&lt;/h2&gt;
&lt;p&gt;After a bit of research, I decided to buy the GL-Inet &lt;a href=&#34;https://www.gl-inet.com/products/gl-mt3000/&#34;&gt;Beryl AX&lt;/a&gt;. It was the right mix between form factor, features, and price. Honestly, I rather wanted to have the excellent-looking &lt;a href=&#34;https://firewalla.com/products/firewalla-purple&#34;&gt;Firewalla Purple&lt;/a&gt;, but that was $99 vs $329, so the Beryl is what I ended up getting.&lt;/p&gt;
&lt;p&gt;What&amp;rsquo;s really cool about the Beryl, besides its small form factor, is that it can support multiple WAN backbones, and fallback or load balance between them. One of those WANs is the WiFi Repeater: The Beryl can hook into any available WiFi network and use that as backbone to create its own WiFi network. That makes it an excellent option for cafes and hotels that likely won&amp;rsquo;t give you an Ethernet port to use.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./smol-wifi.jpg&#34; alt=&#34;My approprietely named WiFi&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;But where it gets even more convenient is that the Beryl also supports USB tethering, so I can plug my iPhone or Android device with unlimited data SIM, and tell the Beryl to use the USB tethered connection as another WAN, then set it to either fallback between Hotel/Cafe wifi and USB tethered connection or just load balance between them for extra bandwidth.&lt;/p&gt;
&lt;p&gt;This also means that I can either repeat my mobile hotspot from my phone or use the USB-tethered connection alone to create a full WiFi network that all my devices can use, for when the Airbnb or Cafe WiFi is especially spotty, without having to change the WiFi network on the devices. Bonus points for the USB port being USB3 and being able to charge my phone while the connection is tethered (USB tethering is something the Firewalla Purple for example can&amp;rsquo;t do).&lt;/p&gt;
&lt;p&gt;That alone would have been enough to make me happy, but there are a few more cool tricks it can do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;iOS and Android apps to manage the device - no need to use the web-based admin UI&lt;/li&gt;
&lt;li&gt;Connect to &lt;a href=&#34;https://ref.nordvpn.com/pBIZKtBKUgi&#34;&gt;NordVPN, my VPN provider of choice&lt;/a&gt; (or other OpenVPN/Wireguard providers). Many people use this to connect to their portable network to their home network&lt;/li&gt;
&lt;li&gt;Separate main and guest networks (for your friends) which can be on separate VLANs and isolated, or have only one of them go through the VPN&lt;/li&gt;
&lt;li&gt;Connect to my Tailscale tailnet, and even use other devices as exit nodes&lt;/li&gt;
&lt;li&gt;Baked-in NextDNS and DNS-over-TLS/HTTPS support&lt;/li&gt;
&lt;li&gt;Mac Address randomization and cloning (for those pesky networks with Mac address filter)&lt;/li&gt;
&lt;li&gt;Dynamic DNS updating&lt;/li&gt;
&lt;li&gt;WiFi 6 and AdGuard (not using this personally because of NextDNS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Beryl is OpenWRT based, so you get full SSH access and Luci interface on top of the GL-Inet admin UI, with very fine-grained control over inner workings like firewall and DNS.&lt;/p&gt;
&lt;p&gt;Then lastly, why I ended up packing this router a lot more than I thought I would: It can be powered through USB-C, so any power bank with USB-PD will work great (or just connecting it to my Macbook). I had it powered in my backpack quite a few times while in Cafes. My 10,000 mAH Anker can usually power it for about 6-8 hours without problems.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./beryl-in-backpack.jpg&#34; alt=&#34;beryl-in-backpack&#34; /&gt;

&lt;/p&gt;
&lt;h2 id=&#34;the-not-so-great&#34;&gt;The not-so-great&lt;/h2&gt;
&lt;p&gt;All of the stuff I wrote above is honestly great&amp;hellip; if it would just work reliably. And this is where I started getting frustrated with this particular device: Reliability. It can be really damn finicky, and there is often a &amp;ldquo;but&amp;rdquo; attached to the great parts.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NextDNS and DNS-over-TLS are great, but of course, this also means captive portals just won&amp;rsquo;t work because they can&amp;rsquo;t hijack the DNS queries. When disabling NextDNS and switching back to auto, then restarting the router, it just forgets your NextDNS ID so you can&amp;rsquo;t just switch it back on&lt;/li&gt;
&lt;li&gt;Tailscale support baked in is great, but it required a lot of messing with OpenWRT firewalls to have it work properly. MagicDNS works when setting up custom DNS forwarding rules in Luci on the Tailscale domain, but those get wiped whenever you switch the DNS to auto or back to NextDNS in the GL-Inet UI&lt;/li&gt;
&lt;li&gt;Multi-WAN is great, but it often tells me that it doesn&amp;rsquo;t have an internet connection on the WAN for whatever reason. Could be an issue with the power source I&amp;rsquo;m using (people on the forums suggested that), could be with the connection, could be a bug when using tethered connection + repeater at the same time (another suggestion from the forums) - I don&amp;rsquo;t know&lt;/li&gt;
&lt;li&gt;NordVPN is great, but it can&amp;rsquo;t refresh configurations automatically and connect to the optimal server in a region. I can set it up to connect to, for example, &lt;code&gt;au531.nordvpn.com.udp&lt;/code&gt;, but then it will always connect to that server&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;More often I spend the first 10-20 minutes of my session just tweaking settings on the admin UI until my connection finally worked, and often wondered why I&amp;rsquo;m not just sharing my phone connection to my Macbook directly instead of going through the Beryl. I had it countless times that devices connected to my phone directly worked flawlessly, but devices that go through the Beryl which goes through my phone don&amp;rsquo;t for whatever reason when they should (may have been an issue with the power cable I used, or a bug with multi-WAN when tethering + repeater, or something else).&lt;/p&gt;
&lt;p&gt;Maybe I&amp;rsquo;m running into these issues because I&amp;rsquo;m trying to do more with it than using it as a dumb travel router, but if those features are offered, I of course want to play with them. I&amp;rsquo;m hoping for a set-and-forget setup that I can just plug into a power outlet and it does the rest, stuff like when there&amp;rsquo;s a captive portal it temporarily disables DNS-over-TLS, or the VPN connection. I had to bookmark the admin interface IP because of how often I had to open in the past weeks just to debug something, which is not a great indicator.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./beryl-in-cafe.jpg&#34; alt=&#34;beryl-in-cafe&#34; /&gt;

&lt;/p&gt;
&lt;h2 id=&#34;so-am-i-happy-with-it&#34;&gt;So, am I happy with it?&lt;/h2&gt;
&lt;p&gt;If I had to give a verdict, I would say: Yes, I am very happy with this device. It has its quirks, but once set up, it runs smoothly, and having a travel router with me gave me a new way to think about networking while on the go.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m writing this blogpost while sitting in an Airbnb connected to it. I jacked the Beryl with an Ethernet cable into the existing router (that only supports 2.4G by the way) and created my own network on top of the existing one. That&amp;rsquo;s where the picture above is coming from.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./beryl-in-airbnb.jpg&#34; alt=&#34;beryl-in-airbnb&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;The router here is sadly not a fiber-based one, but one of those 4G stations, so the networking speed fluctuates drastically. When the connection was particularly bad, I connected my Android phone to the Beryl (Android phone is set to start in tethering mode when USB is connected) and immediately improved the connection for all my devices. That is honestly a damn cool option to have, kind of like a modular connection upgrade.&lt;/p&gt;
&lt;p&gt;Another time I had to restore a few bigger files on a bad guesthouse network. The tethered connection alone wasn&amp;rsquo;t enough, so I used a USB-C Ethernet dongle, connected both my iPhone with 5G SIM (tethered) and my Android with unlimited 4G SIM (ethernet) on different cellular networks to the router at the same time, then set it to load balance between all 3 with priority on the unlimited SIM -&amp;gt; 5G SIM -&amp;gt; WiFi network. I was easily able to finish my bigger download with this burst of additional bandwidth.&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t think I would need a travel router, but I&amp;rsquo;m glad I bought it. It&amp;rsquo;s been only a month, but the small Beryl is already becoming a staple in my travel bag.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/teaser.jpg" /></p><p>Travel routers are a thing I didn&rsquo;t know existed. It makes sense that they do when you think about it, but I was never in a situation where one might have been necessary. Recently, I started a months-long sabbatical in Australia, and part of my research of what to pack on subreddits like /r/onebag landed me on travel routers!</p>
<p>After a bit of research, I went out and ordered the GL-Inet Beryl AX, a small cute mint-colored box that would fit into my pocket if I wanted to.</p>
<h2 id="what-are-travel-routers-and-why-i-wanted-one">What are travel routers, and why I wanted one</h2>
<p>As the name implied - routers for traveling (duh). They are usually very small, and feature WiFi and an ethernet port. More advanced models have more functionality like VPN, Wireguard tunnel, and whatnot, depending on your needs.</p>
<p>The appeal for me is bringing my own network with me. If I hop between hotels, Airbnbs, and cafes, I don&rsquo;t have to worry about setting up all my devices to use that network again and again - all I have to set up is the travel router and the rest <em>just works</em>. The router can also add another layer of security with things like DNS-over-HTTPS, OpenVPN connections, or tailscale Wireguard tunnel on the router level, so none of my end devices have to know about any of that. I have one less thing I have to trust (to some extent).</p>
<p>Another, maybe more silly reason was mDNS and Chromecast: One of the devices I wanted to bring with me was my small Chromecast because of its size, but those devices usually just won&rsquo;t work with Hotel WiFis because they disable mDNS (not to mention the PITA to keep setting it up with new Airbnb wifis). If I have my own pocket WiFi with me, however, I can use the Chromecast just fine, for the occasional movie session.</p>
<p>All in all, a travel router I can pocket sounded like a good idea.</p>
<h2 id="the-beryl-ax-and-what-it-can-do">The Beryl AX, and what it can do</h2>
<p>After a bit of research, I decided to buy the GL-Inet <a href="https://www.gl-inet.com/products/gl-mt3000/">Beryl AX</a>. It was the right mix between form factor, features, and price. Honestly, I rather wanted to have the excellent-looking <a href="https://firewalla.com/products/firewalla-purple">Firewalla Purple</a>, but that was $99 vs $329, so the Beryl is what I ended up getting.</p>
<p>What&rsquo;s really cool about the Beryl, besides its small form factor, is that it can support multiple WAN backbones, and fallback or load balance between them. One of those WANs is the WiFi Repeater: The Beryl can hook into any available WiFi network and use that as backbone to create its own WiFi network. That makes it an excellent option for cafes and hotels that likely won&rsquo;t give you an Ethernet port to use.</p>
<p>
<img class="img-fluid" src="./smol-wifi.jpg" alt="My approprietely named WiFi" />

</p>
<p>But where it gets even more convenient is that the Beryl also supports USB tethering, so I can plug my iPhone or Android device with unlimited data SIM, and tell the Beryl to use the USB tethered connection as another WAN, then set it to either fallback between Hotel/Cafe wifi and USB tethered connection or just load balance between them for extra bandwidth.</p>
<p>This also means that I can either repeat my mobile hotspot from my phone or use the USB-tethered connection alone to create a full WiFi network that all my devices can use, for when the Airbnb or Cafe WiFi is especially spotty, without having to change the WiFi network on the devices. Bonus points for the USB port being USB3 and being able to charge my phone while the connection is tethered (USB tethering is something the Firewalla Purple for example can&rsquo;t do).</p>
<p>That alone would have been enough to make me happy, but there are a few more cool tricks it can do:</p>
<ul>
<li>iOS and Android apps to manage the device - no need to use the web-based admin UI</li>
<li>Connect to <a href="https://ref.nordvpn.com/pBIZKtBKUgi">NordVPN, my VPN provider of choice</a> (or other OpenVPN/Wireguard providers). Many people use this to connect to their portable network to their home network</li>
<li>Separate main and guest networks (for your friends) which can be on separate VLANs and isolated, or have only one of them go through the VPN</li>
<li>Connect to my Tailscale tailnet, and even use other devices as exit nodes</li>
<li>Baked-in NextDNS and DNS-over-TLS/HTTPS support</li>
<li>Mac Address randomization and cloning (for those pesky networks with Mac address filter)</li>
<li>Dynamic DNS updating</li>
<li>WiFi 6 and AdGuard (not using this personally because of NextDNS</li>
</ul>
<p>The Beryl is OpenWRT based, so you get full SSH access and Luci interface on top of the GL-Inet admin UI, with very fine-grained control over inner workings like firewall and DNS.</p>
<p>Then lastly, why I ended up packing this router a lot more than I thought I would: It can be powered through USB-C, so any power bank with USB-PD will work great (or just connecting it to my Macbook). I had it powered in my backpack quite a few times while in Cafes. My 10,000 mAH Anker can usually power it for about 6-8 hours without problems.</p>
<p>
<img class="img-fluid" src="./beryl-in-backpack.jpg" alt="beryl-in-backpack" />

</p>
<h2 id="the-not-so-great">The not-so-great</h2>
<p>All of the stuff I wrote above is honestly great&hellip; if it would just work reliably. And this is where I started getting frustrated with this particular device: Reliability. It can be really damn finicky, and there is often a &ldquo;but&rdquo; attached to the great parts.</p>
<p>For example:</p>
<ul>
<li>NextDNS and DNS-over-TLS are great, but of course, this also means captive portals just won&rsquo;t work because they can&rsquo;t hijack the DNS queries. When disabling NextDNS and switching back to auto, then restarting the router, it just forgets your NextDNS ID so you can&rsquo;t just switch it back on</li>
<li>Tailscale support baked in is great, but it required a lot of messing with OpenWRT firewalls to have it work properly. MagicDNS works when setting up custom DNS forwarding rules in Luci on the Tailscale domain, but those get wiped whenever you switch the DNS to auto or back to NextDNS in the GL-Inet UI</li>
<li>Multi-WAN is great, but it often tells me that it doesn&rsquo;t have an internet connection on the WAN for whatever reason. Could be an issue with the power source I&rsquo;m using (people on the forums suggested that), could be with the connection, could be a bug when using tethered connection + repeater at the same time (another suggestion from the forums) - I don&rsquo;t know</li>
<li>NordVPN is great, but it can&rsquo;t refresh configurations automatically and connect to the optimal server in a region. I can set it up to connect to, for example, <code>au531.nordvpn.com.udp</code>, but then it will always connect to that server</li>
</ul>
<p>More often I spend the first 10-20 minutes of my session just tweaking settings on the admin UI until my connection finally worked, and often wondered why I&rsquo;m not just sharing my phone connection to my Macbook directly instead of going through the Beryl. I had it countless times that devices connected to my phone directly worked flawlessly, but devices that go through the Beryl which goes through my phone don&rsquo;t for whatever reason when they should (may have been an issue with the power cable I used, or a bug with multi-WAN when tethering + repeater, or something else).</p>
<p>Maybe I&rsquo;m running into these issues because I&rsquo;m trying to do more with it than using it as a dumb travel router, but if those features are offered, I of course want to play with them. I&rsquo;m hoping for a set-and-forget setup that I can just plug into a power outlet and it does the rest, stuff like when there&rsquo;s a captive portal it temporarily disables DNS-over-TLS, or the VPN connection. I had to bookmark the admin interface IP because of how often I had to open in the past weeks just to debug something, which is not a great indicator.</p>
<p>
<img class="img-fluid" src="./beryl-in-cafe.jpg" alt="beryl-in-cafe" />

</p>
<h2 id="so-am-i-happy-with-it">So, am I happy with it?</h2>
<p>If I had to give a verdict, I would say: Yes, I am very happy with this device. It has its quirks, but once set up, it runs smoothly, and having a travel router with me gave me a new way to think about networking while on the go.</p>
<p>I&rsquo;m writing this blogpost while sitting in an Airbnb connected to it. I jacked the Beryl with an Ethernet cable into the existing router (that only supports 2.4G by the way) and created my own network on top of the existing one. That&rsquo;s where the picture above is coming from.</p>
<p>
<img class="img-fluid" src="./beryl-in-airbnb.jpg" alt="beryl-in-airbnb" />

</p>
<p>The router here is sadly not a fiber-based one, but one of those 4G stations, so the networking speed fluctuates drastically. When the connection was particularly bad, I connected my Android phone to the Beryl (Android phone is set to start in tethering mode when USB is connected) and immediately improved the connection for all my devices. That is honestly a damn cool option to have, kind of like a modular connection upgrade.</p>
<p>Another time I had to restore a few bigger files on a bad guesthouse network. The tethered connection alone wasn&rsquo;t enough, so I used a USB-C Ethernet dongle, connected both my iPhone with 5G SIM (tethered) and my Android with unlimited 4G SIM (ethernet) on different cellular networks to the router at the same time, then set it to load balance between all 3 with priority on the unlimited SIM -&gt; 5G SIM -&gt; WiFi network. I was easily able to finish my bigger download with this burst of additional bandwidth.</p>
<p>I didn&rsquo;t think I would need a travel router, but I&rsquo;m glad I bought it. It&rsquo;s been only a month, but the small Beryl is already becoming a staple in my travel bag.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Building your own distributed CLI ChatGPT in Elixir with GenServer</title>
      <link>https://david.coffee/mini-chatgpt-in-elixir-and-genserver/</link>
      <pubDate>Mon, 13 Mar 2023 10:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/mini-chatgpt-in-elixir-and-genserver/</guid><media:content url="https://david.coffee/pocketrobot.png" medium="image" />
          <enclosure url="https://david.coffee/pocketrobot.png" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/pocketrobot.png&#34; /&gt;&lt;/p&gt;&lt;p&gt;I recently built an &lt;a href=&#34;https://github.com/dvcrn/ex_openai&#34;&gt;Elixir SDK for OpenAI APIs&lt;/a&gt; for a project I was working on to automatically fix and correct Japanese grammar (&lt;a href=&#34;https://fixmyjp.d.sh&#34;&gt;https://fixmyjp.d.sh&lt;/a&gt;). The SDK is actually fully auto-generated with metaprogramming which is something I wanted to do in Elixir for a while, but that&amp;rsquo;s not the topic of today&amp;rsquo;s post.&lt;/p&gt;
&lt;p&gt;Today, let&amp;rsquo;s use that SDK for something fun and build a smol ChatGPT service for your shell from scratch, in true Elixir fashion!&lt;/p&gt;
&lt;p&gt;The end result will be an &lt;code&gt;ai&lt;/code&gt; shell command that is usable from wherever we want, to send messages to a distributed &lt;code&gt;gptserver&lt;/code&gt; node for interacting with ChatGPT, that can either be on the same computer, or anywhere on the same network&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./finalshell.png&#34; alt=&#34;finalshell&#34; /&gt;

&lt;/p&gt;
&lt;h2 id=&#34;quick-refresher-genservers-and-how-state-in-elixir-is-implemented&#34;&gt;Quick refresher: GenServers, and how state in Elixir is implemented&lt;/h2&gt;
&lt;p&gt;Elixir is an &lt;strong&gt;immutable&lt;/strong&gt; functional programming language, meaning you can&amp;rsquo;t mutate things. If you do&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-elixir&#34; data-lang=&#34;elixir&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;a &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;a &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; a &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;you are not mutating &lt;code&gt;a&lt;/code&gt; to be a+1, instead you create a new &lt;code&gt;a&lt;/code&gt; with the value of a+1. That means you can&amp;rsquo;t do things like this in Elixir:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-elixir&#34; data-lang=&#34;elixir&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;a &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; %{}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;a[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;foo&amp;#34;&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;bar&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;instead, you would do:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-elixir&#34; data-lang=&#34;elixir&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;a &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; %{}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;a &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Map&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;put(a, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;foo&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;bar&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;again, creating a new &lt;code&gt;a&lt;/code&gt; with a copy of the previous a, but one item added. The old &lt;code&gt;a&lt;/code&gt; is not mutated, it still exists until the garbage collector gets rid of it.&lt;/p&gt;
&lt;p&gt;So how tf do you actually keep any state across different modules then? From the sounds of it, you would need to constantly do something with a value, you can&amp;rsquo;t just create an object that different parts of the code can mutate and share.&lt;/p&gt;
&lt;p&gt;In Elixir/Erlang-land, instead of having a shared object/container somewhere, we use &lt;em&gt;processes&lt;/em&gt; managed within the BEAM VM, and the most common way to define those processes is &lt;code&gt;GenServer&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;GenServer is a behaviour that looks like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-elixir&#34; data-lang=&#34;elixir&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;defmodule&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Foo&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;use&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;GenServer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#75715e&#34;&gt;# stuff that&amp;#39;s running inside a separate process&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;@impl&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; init(val), &lt;span style=&#34;color:#e6db74&#34;&gt;do&lt;/span&gt;: {&lt;span style=&#34;color:#e6db74&#34;&gt;:ok&lt;/span&gt;, val}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;@impl&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; handle_call({&lt;span style=&#34;color:#e6db74&#34;&gt;:add&lt;/span&gt;, i}, _from, sum) &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  {&lt;span style=&#34;color:#e6db74&#34;&gt;:reply&lt;/span&gt;, sum&lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt;i, sum&lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt;i}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#75715e&#34;&gt;# stuff that&amp;#39;s running inside the main process&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; add(pid, num) &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  &lt;span style=&#34;color:#a6e22e&#34;&gt;GenServer&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;call(pid, {&lt;span style=&#34;color:#e6db74&#34;&gt;:add&lt;/span&gt;, num})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;foo &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;GenServer&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;start_link(&lt;span style=&#34;color:#a6e22e&#34;&gt;Foo&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Foo&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;add(foo, &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;) &lt;span style=&#34;color:#75715e&#34;&gt;# 0 + 2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Foo&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;add(foo, &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;) &lt;span style=&#34;color:#75715e&#34;&gt;# 2 + 3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To hold state, we effectively created a new process whose whole purpose is to hold a value in memory, then listen for a message &lt;code&gt;:add&lt;/code&gt;, to then add whatever got passed to create a &lt;strong&gt;new value&lt;/strong&gt; (remember? we can&amp;rsquo;t mutate things) to hold in memory and start listening for new messages again. Imagine it as a (&lt;em&gt;very&lt;/em&gt; simplified representation):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;startValue&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;startValue&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;waitForUserInput&lt;/span&gt;()); &lt;span style=&#34;color:#75715e&#34;&gt;// yay, no variables
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;level-1-chatgpt-in-a-genserver&#34;&gt;Level 1: ChatGPT in a GenServer&lt;/h2&gt;
&lt;p&gt;Okay now that we remembered how to handle state in Elixir, we know what to do to get a ChatGPT-like system going: &lt;code&gt;Wait for user input -&amp;gt; append to messages so far -&amp;gt; send to OpenAI -&amp;gt; output -&amp;gt; wait for user input again&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s summon a new mix project:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;mix new mixgpt
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;add my &lt;code&gt;ex_openai&lt;/code&gt; library as deps to &lt;code&gt;mix.exs&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-elixir&#34; data-lang=&#34;elixir&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;defp&lt;/span&gt; deps &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {&lt;span style=&#34;color:#e6db74&#34;&gt;:ex_openai&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;gt;= 1.0.2&amp;#34;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(follow the &lt;a href=&#34;https://github.com/dvcrn/ex_openai&#34;&gt;instructions for configuration&lt;/a&gt; with an API key)&lt;/p&gt;
&lt;p&gt;and conjure up a basic GenServer with a new &lt;code&gt;{:msg, m}&lt;/code&gt; message listener:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-elixir&#34; data-lang=&#34;elixir&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;defmodule&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Mixgpt&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;use&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;GenServer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;@impl&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; init(_opts) &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  {&lt;span style=&#34;color:#e6db74&#34;&gt;:ok&lt;/span&gt;, []} &lt;span style=&#34;color:#75715e&#34;&gt;# start value is empty list&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;@impl&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; handle_call({&lt;span style=&#34;color:#e6db74&#34;&gt;:msg&lt;/span&gt;, m}, _from, msgs) &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To make our life easier, we&amp;rsquo;ll also add a &lt;code&gt;start_link&lt;/code&gt; function to start the thing:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-elixir&#34; data-lang=&#34;elixir&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; start_link(_opts) &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;GenServer&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;start_link(__MODULE__, [], &lt;span style=&#34;color:#e6db74&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;:gptserver&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Notice the &lt;code&gt;name: :gptserver&lt;/code&gt; argument here - we&amp;rsquo;re giving the GenServer a unique name so that we don&amp;rsquo;t need to worry about remembering its PID somewhere.&lt;/p&gt;
&lt;p&gt;To send a message to this new genserver, we&amp;rsquo;ll also create a new &lt;code&gt;send()&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-elixir&#34; data-lang=&#34;elixir&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; send(msg) &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;GenServer&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;call(&lt;span style=&#34;color:#e6db74&#34;&gt;:gptserver&lt;/span&gt;, {&lt;span style=&#34;color:#e6db74&#34;&gt;:msg&lt;/span&gt;, msg})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;thanks to &lt;code&gt;name: :gptserver&lt;/code&gt; we already know how to reach this instance, so we no longer need the PID to be passed. Now, all we have to do is &lt;code&gt;Mixgpt.send &amp;quot;hi!&amp;quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;In the actual &lt;code&gt;handle_call&lt;/code&gt;, what we have to do is: construct a new OpenAI ChatGPT message with the passed message, send it to the OpenAI API, and add whatever it returned to the internal state of this server (remember from above? how to handle state). Something like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-elixir&#34; data-lang=&#34;elixir&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;defp&lt;/span&gt; new_msg(m) &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  %&lt;span style=&#34;color:#a6e22e&#34;&gt;ExOpenAI.Components.ChatCompletionRequestMessage&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;content&lt;/span&gt;: m,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;role&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;:user&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@impl&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; handle_call({&lt;span style=&#34;color:#e6db74&#34;&gt;:msg&lt;/span&gt;, m}, _from, msgs) &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# create temporary &amp;#34;msgs&amp;#34; with existing list + the new message from the user&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# effectively appending the new message to the existing messages&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  with msgs &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;-&lt;/span&gt; msgs &lt;span style=&#34;color:#f92672&#34;&gt;++&lt;/span&gt; [new_msg(m)] &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	  &lt;span style=&#34;color:#75715e&#34;&gt;# call OpenAI ChatGPT API with the new msgs list&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ExOpenAI.Chat&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;create_chat_completion(msgs, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;gpt-3.5-turbo&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      {&lt;span style=&#34;color:#e6db74&#34;&gt;:ok&lt;/span&gt;, res} &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	      &lt;span style=&#34;color:#75715e&#34;&gt;# return the content of the result, add it to the msgs list and continue the GenServer loop with {:reply}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        first &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;List&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;first(res&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;choices)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# second value is what&amp;#39;s returned, third value is the new state of this server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {&lt;span style=&#34;color:#e6db74&#34;&gt;:reply&lt;/span&gt;, first&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;message&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;content, msgs &lt;span style=&#34;color:#f92672&#34;&gt;++&lt;/span&gt; [first&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;message]}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      {&lt;span style=&#34;color:#e6db74&#34;&gt;:error&lt;/span&gt;, reason} &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {&lt;span style=&#34;color:#e6db74&#34;&gt;:error&lt;/span&gt;, reason}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The cool thing about Elixir and other functional languages is that they usually come with a REPL, or a shell that you can use to interact with different bits and pieces of your program. In Elixir, that&amp;rsquo;s &lt;code&gt;iex&lt;/code&gt;. Run &lt;code&gt;iex -S mix&lt;/code&gt;, wait until it starts, then try to interact with our new GenServer:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-elixir&#34; data-lang=&#34;elixir&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;iex&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Mixgpt&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;start_link(&lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{&lt;span style=&#34;color:#e6db74&#34;&gt;:ok&lt;/span&gt;, &lt;span style=&#34;color:#75715e&#34;&gt;#PID&amp;lt;0.280.0&amp;gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;iex&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Mixgpt&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;send &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Hi!&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Hello! How can I assist you today?&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;iex&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Mixgpt&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;send &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tell me a joke in 5 words&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;\&amp;#34;Why did the tomato blush?\&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Cool, now we already got our own pocket ChatGPT that we can access through mix, that was pretty quick, wasn&amp;rsquo;t it? Let&amp;rsquo;s make it a bit more automatic and close the loop. To wait for user input, we can use &lt;code&gt;IO.gets&lt;/code&gt;, let&amp;rsquo;s use that in a new function to directly ask for the next message:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-elixir&#34; data-lang=&#34;elixir&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; wait_for_input_and_send() &lt;span style=&#34;color:#66d9ef&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;IO&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;gets(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;gt; &amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;|&amp;gt;&lt;/span&gt; send
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;|&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;IO&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;puts() &lt;span style=&#34;color:#75715e&#34;&gt;# print the message out to have it nicely formatted&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  wait_for_input_and_send() &lt;span style=&#34;color:#75715e&#34;&gt;# recurse&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Type &lt;code&gt;recompile&lt;/code&gt; into the iex shell to recompile the project and try again:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-elixir&#34; data-lang=&#34;elixir&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;iex&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Mixgpt&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;wait_for_input_and_send
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Hi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Hello&lt;/span&gt;, how can &lt;span style=&#34;color:#a6e22e&#34;&gt;I&lt;/span&gt; assist you today?
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Do&lt;/span&gt; you like &lt;span style=&#34;color:#a6e22e&#34;&gt;Elixir&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;As&lt;/span&gt; an &lt;span style=&#34;color:#a6e22e&#34;&gt;AI&lt;/span&gt; language model, &lt;span style=&#34;color:#a6e22e&#34;&gt;I&lt;/span&gt; don&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;t have personal preferences or feelings. However, I can tell you that Elixir is a popular programming language among developers and has some unique features that make it stand out, such as its scalability, fault tolerance, and ease of use for distributed systems.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;gt; what was the first message I sent you?
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;The first message you sent me was &amp;#34;hi&amp;#34;.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;level-2-distributed-chatgpt-service&#34;&gt;Level 2: Distributed ChatGPT service&lt;/h2&gt;
&lt;p&gt;The example above is easy to do in any programming language, but let&amp;rsquo;s go one step further and do something cool that you can do beautifully in Elixir and Erlang, but not as easily in other programming languages: &lt;strong&gt;Let&amp;rsquo;s turn this thing into a proper distributed system!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;As a reminder, our mini chatgpt is running inside a &lt;code&gt;server&lt;/code&gt;! We did this because that&amp;rsquo;s how state is handled in Elixir. Even though we created the interface so that we can easily just call &lt;code&gt;send(&amp;quot;why is the sky green?&amp;quot;)&lt;/code&gt;, on the back it still sends a new message to this gpt-server, then waits until it responds. It just does this so transparently that we don&amp;rsquo;t even notice.&lt;/p&gt;
&lt;p&gt;But what&amp;rsquo;s even cooler: GenServers can already be accessed from other nodes within same BEAM cluster, so we &lt;em&gt;already have&lt;/em&gt; something that is usable over a network! Let&amp;rsquo;s take a look at how to do that, with only a little bit of black magic.&lt;/p&gt;
&lt;p&gt;Close the iex shell we had open, and create a new one, but this time with the &lt;code&gt;--name&lt;/code&gt; parameter:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;iex --name gptserver@127.0.0.1 -S mix
iex&amp;gt; Mixgpt.start_link
{:ok,  #PID&amp;lt;0.276.0&amp;gt;}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;--name&lt;/code&gt; tells elixir to start a distributed node with the given name, on the given network. So we created a distributed node &lt;code&gt;gptserver&lt;/code&gt; on the network &lt;code&gt;127.0.0.1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;In a new terminal window, start another iex session, this time without &lt;code&gt;-S mix&lt;/code&gt;, but still with &lt;code&gt;--name&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;iex --name client@127.0.0.1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To now execute things on another cluster node, Elixir provides the &lt;code&gt;Node.spawn_link&lt;/code&gt; function among other things. With this you could do stuff like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-elixir&#34; data-lang=&#34;elixir&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;iex&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Node&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;spawn_link(&lt;span style=&#34;color:#e6db74&#34;&gt;:&amp;#34;gptserver@127.0.0.1&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;fn&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;IO&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;puts(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;hi there, I&amp;#39;m running on another node&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;end&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But we already have a GenServer running, so no need for that and just hit the GenServer directly. This is also very easy to do, just by changing the &lt;code&gt;GenServer.call(:gptserver, {:msg, msg})&lt;/code&gt; call to also include the node information where &lt;code&gt;:gptserver&lt;/code&gt; is running: &lt;code&gt;{:gptserver, :&amp;quot;gptserver@127.0.0.1&amp;quot;}&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-elixir&#34; data-lang=&#34;elixir&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;iex&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;GenServer&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;call({&lt;span style=&#34;color:#e6db74&#34;&gt;:gptserver&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;:&amp;#34;gptserver@127.0.0.1&amp;#34;&lt;/span&gt;}, {&lt;span style=&#34;color:#e6db74&#34;&gt;:msg&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Helloo, anyone there?&amp;#34;&lt;/span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Hello! Yes, I&amp;#39;m here. How can I assist you?&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Do you realize what just happened? Our completely separate &lt;code&gt;iex&lt;/code&gt; shells just connected to each other to send a message to the GenServer running under the &lt;code&gt;:gptserver&lt;/code&gt; name, which then hits the OpenAI API, returns the data, and relays it back to the other &lt;code&gt;iex&lt;/code&gt; shell!&lt;/p&gt;
&lt;p&gt;We just created a distributed ChatGPT service on our local network (well, localhost), all without any extra setup required! This even works across machines, as long as the nodes are running reachable on the same network :)&lt;/p&gt;
&lt;h2 id=&#34;final-step-a-global-shell-command&#34;&gt;Final step: A global shell command&lt;/h2&gt;
&lt;p&gt;You can already guess what comes next. We can run Elixir code directly with &lt;code&gt;elixir -e &amp;quot;IO.puts \&amp;quot;Hi\&amp;quot;&amp;quot;&lt;/code&gt;. The last lego piece missing is hooking this up to a global shell command, so that we can access our mini-ChatGPT whenever we need through a terminal, while retaining previous context.&lt;/p&gt;
&lt;p&gt;To package it all up into a one-liner:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$ elixir --name client@127.0.0.1 -e &amp;#34;GenServer.call({:gptserver, :&amp;#39;gptserver@127.0.0.1&amp;#39;}, {:msg, \&amp;#34;hi\&amp;#34;}) |&amp;gt; IO.puts&amp;#34;
Hello! How can I assist you today?
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That&amp;rsquo;s still the same call as before, except we&amp;rsquo;re running it through &lt;code&gt;elixir -e &amp;quot;&amp;quot;&lt;/code&gt;, and pipe the result into &lt;code&gt;IO.puts&lt;/code&gt; to write it to the shell. This is still using the same GenServer that&amp;rsquo;s still running (unless you shut it down), and it still retains the same message history from earlier.&lt;/p&gt;
&lt;p&gt;Lastly, package into a bash function:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;ai() { elixir --name client@127.0.0.1 -e &amp;#34;GenServer.call({:gptserver, :&amp;#39;gptserver@127.0.0.1&amp;#39;}, {:msg, Enum.join(System.argv, \&amp;#34; \&amp;#34;)}) |&amp;gt; IO.puts&amp;#34;; $@;  };
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Tadaa! We now have a system-wide (or network-wide) mini-ChatGPT that is fully &lt;strong&gt;stateful&lt;/strong&gt;&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$ ai is the sky blue?
Yes, on clear days, the daytime sky typically appears blue due to the scattering of sunlight by Earth&amp;#39;s atmosphere.
$ ai what is the last message I sent you?
Your last message was &amp;#34;is the sky blue?&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We could easily put this on a home server and have it available across all of our machines without any extra setup if we really wanted to&lt;/p&gt;
&lt;p&gt;Of course, this is still super barebones and in reality, we&amp;rsquo;d do a few more things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A Supervisor to handle crashes&lt;/li&gt;
&lt;li&gt;Changing &lt;code&gt;:gptserver&lt;/code&gt; to be global, so we don&amp;rsquo;t need to know the hostname&lt;/li&gt;
&lt;li&gt;Some way to reset, or clear the message history to not have messages endlessly pile up&lt;/li&gt;
&lt;li&gt;Better error handling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;among others&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/dvcrn/elixir-cli-chatgpt&#34;&gt;Check out the code for this on GitHub&lt;/a&gt;&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/pocketrobot.png" /></p><p>I recently built an <a href="https://github.com/dvcrn/ex_openai">Elixir SDK for OpenAI APIs</a> for a project I was working on to automatically fix and correct Japanese grammar (<a href="https://fixmyjp.d.sh">https://fixmyjp.d.sh</a>). The SDK is actually fully auto-generated with metaprogramming which is something I wanted to do in Elixir for a while, but that&rsquo;s not the topic of today&rsquo;s post.</p>
<p>Today, let&rsquo;s use that SDK for something fun and build a smol ChatGPT service for your shell from scratch, in true Elixir fashion!</p>
<p>The end result will be an <code>ai</code> shell command that is usable from wherever we want, to send messages to a distributed <code>gptserver</code> node for interacting with ChatGPT, that can either be on the same computer, or anywhere on the same network</p>
<p>
<img class="img-fluid" src="./finalshell.png" alt="finalshell" />

</p>
<h2 id="quick-refresher-genservers-and-how-state-in-elixir-is-implemented">Quick refresher: GenServers, and how state in Elixir is implemented</h2>
<p>Elixir is an <strong>immutable</strong> functional programming language, meaning you can&rsquo;t mutate things. If you do</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-elixir" data-lang="elixir"><span style="display:flex;"><span>a <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>a <span style="color:#f92672">=</span> a <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>
</span></span></code></pre></div><p>you are not mutating <code>a</code> to be a+1, instead you create a new <code>a</code> with the value of a+1. That means you can&rsquo;t do things like this in Elixir:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-elixir" data-lang="elixir"><span style="display:flex;"><span>a <span style="color:#f92672">=</span> %{}
</span></span><span style="display:flex;"><span>a[<span style="color:#e6db74">&#34;foo&#34;</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;bar&#34;</span>
</span></span></code></pre></div><p>instead, you would do:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-elixir" data-lang="elixir"><span style="display:flex;"><span>a <span style="color:#f92672">=</span> %{}
</span></span><span style="display:flex;"><span>a <span style="color:#f92672">=</span> <span style="color:#a6e22e">Map</span><span style="color:#f92672">.</span>put(a, <span style="color:#e6db74">&#34;foo&#34;</span>, <span style="color:#e6db74">&#34;bar&#34;</span>)
</span></span></code></pre></div><p>again, creating a new <code>a</code> with a copy of the previous a, but one item added. The old <code>a</code> is not mutated, it still exists until the garbage collector gets rid of it.</p>
<p>So how tf do you actually keep any state across different modules then? From the sounds of it, you would need to constantly do something with a value, you can&rsquo;t just create an object that different parts of the code can mutate and share.</p>
<p>In Elixir/Erlang-land, instead of having a shared object/container somewhere, we use <em>processes</em> managed within the BEAM VM, and the most common way to define those processes is <code>GenServer</code>.</p>
<p>GenServer is a behaviour that looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-elixir" data-lang="elixir"><span style="display:flex;"><span><span style="color:#66d9ef">defmodule</span> <span style="color:#a6e22e">Foo</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">use</span> <span style="color:#a6e22e">GenServer</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e"># stuff that&#39;s running inside a separate process</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">@impl</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> init(val), <span style="color:#e6db74">do</span>: {<span style="color:#e6db74">:ok</span>, val}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">@impl</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> handle_call({<span style="color:#e6db74">:add</span>, i}, _from, sum) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>	  {<span style="color:#e6db74">:reply</span>, sum<span style="color:#f92672">+</span>i, sum<span style="color:#f92672">+</span>i}
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e"># stuff that&#39;s running inside the main process</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> add(pid, num) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>	  <span style="color:#a6e22e">GenServer</span><span style="color:#f92672">.</span>call(pid, {<span style="color:#e6db74">:add</span>, num})
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>foo <span style="color:#f92672">=</span> <span style="color:#a6e22e">GenServer</span><span style="color:#f92672">.</span>start_link(<span style="color:#a6e22e">Foo</span>, <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Foo</span><span style="color:#f92672">.</span>add(foo, <span style="color:#ae81ff">2</span>) <span style="color:#75715e"># 0 + 2</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Foo</span><span style="color:#f92672">.</span>add(foo, <span style="color:#ae81ff">3</span>) <span style="color:#75715e"># 2 + 3</span>
</span></span></code></pre></div><p>To hold state, we effectively created a new process whose whole purpose is to hold a value in memory, then listen for a message <code>:add</code>, to then add whatever got passed to create a <strong>new value</strong> (remember? we can&rsquo;t mutate things) to hold in memory and start listening for new messages again. Imagine it as a (<em>very</em> simplified representation):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">add</span>(<span style="color:#a6e22e">startValue</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">add</span>(<span style="color:#a6e22e">startValue</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">waitForUserInput</span>()); <span style="color:#75715e">// yay, no variables
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><h2 id="level-1-chatgpt-in-a-genserver">Level 1: ChatGPT in a GenServer</h2>
<p>Okay now that we remembered how to handle state in Elixir, we know what to do to get a ChatGPT-like system going: <code>Wait for user input -&gt; append to messages so far -&gt; send to OpenAI -&gt; output -&gt; wait for user input again</code></p>
<p>Let&rsquo;s summon a new mix project:</p>
<pre tabindex="0"><code>mix new mixgpt
</code></pre><p>add my <code>ex_openai</code> library as deps to <code>mix.exs</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-elixir" data-lang="elixir"><span style="display:flex;"><span><span style="color:#66d9ef">defp</span> deps <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  [
</span></span><span style="display:flex;"><span>    {<span style="color:#e6db74">:ex_openai</span>, <span style="color:#e6db74">&#34;&gt;= 1.0.2&#34;</span>}
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>(follow the <a href="https://github.com/dvcrn/ex_openai">instructions for configuration</a> with an API key)</p>
<p>and conjure up a basic GenServer with a new <code>{:msg, m}</code> message listener:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-elixir" data-lang="elixir"><span style="display:flex;"><span><span style="color:#66d9ef">defmodule</span> <span style="color:#a6e22e">Mixgpt</span> <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">use</span> <span style="color:#a6e22e">GenServer</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">@impl</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> init(_opts) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>	  {<span style="color:#e6db74">:ok</span>, []} <span style="color:#75715e"># start value is empty list</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">@impl</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">def</span> handle_call({<span style="color:#e6db74">:msg</span>, m}, _from, msgs) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>To make our life easier, we&rsquo;ll also add a <code>start_link</code> function to start the thing:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-elixir" data-lang="elixir"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> start_link(_opts) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">GenServer</span><span style="color:#f92672">.</span>start_link(__MODULE__, [], <span style="color:#e6db74">name</span>: <span style="color:#e6db74">:gptserver</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Notice the <code>name: :gptserver</code> argument here - we&rsquo;re giving the GenServer a unique name so that we don&rsquo;t need to worry about remembering its PID somewhere.</p>
<p>To send a message to this new genserver, we&rsquo;ll also create a new <code>send()</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-elixir" data-lang="elixir"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> send(msg) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">GenServer</span><span style="color:#f92672">.</span>call(<span style="color:#e6db74">:gptserver</span>, {<span style="color:#e6db74">:msg</span>, msg})
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>thanks to <code>name: :gptserver</code> we already know how to reach this instance, so we no longer need the PID to be passed. Now, all we have to do is <code>Mixgpt.send &quot;hi!&quot;</code></p>
<p>In the actual <code>handle_call</code>, what we have to do is: construct a new OpenAI ChatGPT message with the passed message, send it to the OpenAI API, and add whatever it returned to the internal state of this server (remember from above? how to handle state). Something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-elixir" data-lang="elixir"><span style="display:flex;"><span><span style="color:#66d9ef">defp</span> new_msg(m) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  %<span style="color:#a6e22e">ExOpenAI.Components.ChatCompletionRequestMessage</span>{
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">content</span>: m,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">role</span>: <span style="color:#e6db74">:user</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">name</span>: <span style="color:#e6db74">&#34;user&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@impl</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> handle_call({<span style="color:#e6db74">:msg</span>, m}, _from, msgs) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># create temporary &#34;msgs&#34; with existing list + the new message from the user</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># effectively appending the new message to the existing messages</span>
</span></span><span style="display:flex;"><span>  with msgs <span style="color:#f92672">&lt;-</span> msgs <span style="color:#f92672">++</span> [new_msg(m)] <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	  <span style="color:#75715e"># call OpenAI ChatGPT API with the new msgs list</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">ExOpenAI.Chat</span><span style="color:#f92672">.</span>create_chat_completion(msgs, <span style="color:#e6db74">&#34;gpt-3.5-turbo&#34;</span>) <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>      {<span style="color:#e6db74">:ok</span>, res} <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>	      <span style="color:#75715e"># return the content of the result, add it to the msgs list and continue the GenServer loop with {:reply}</span>
</span></span><span style="display:flex;"><span>        first <span style="color:#f92672">=</span> <span style="color:#a6e22e">List</span><span style="color:#f92672">.</span>first(res<span style="color:#f92672">.</span>choices)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># second value is what&#39;s returned, third value is the new state of this server</span>
</span></span><span style="display:flex;"><span>        {<span style="color:#e6db74">:reply</span>, first<span style="color:#f92672">.</span>message<span style="color:#f92672">.</span>content, msgs <span style="color:#f92672">++</span> [first<span style="color:#f92672">.</span>message]}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      {<span style="color:#e6db74">:error</span>, reason} <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        {<span style="color:#e6db74">:error</span>, reason}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">end</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>The cool thing about Elixir and other functional languages is that they usually come with a REPL, or a shell that you can use to interact with different bits and pieces of your program. In Elixir, that&rsquo;s <code>iex</code>. Run <code>iex -S mix</code>, wait until it starts, then try to interact with our new GenServer:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-elixir" data-lang="elixir"><span style="display:flex;"><span>iex<span style="color:#f92672">&gt;</span> <span style="color:#a6e22e">Mixgpt</span><span style="color:#f92672">.</span>start_link(<span style="color:#66d9ef">nil</span>)
</span></span><span style="display:flex;"><span>{<span style="color:#e6db74">:ok</span>, <span style="color:#75715e">#PID&lt;0.280.0&gt;}</span>
</span></span><span style="display:flex;"><span>iex<span style="color:#f92672">&gt;</span> <span style="color:#a6e22e">Mixgpt</span><span style="color:#f92672">.</span>send <span style="color:#e6db74">&#34;Hi!&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;Hello! How can I assist you today?&#34;</span>
</span></span><span style="display:flex;"><span>iex<span style="color:#f92672">&gt;</span> <span style="color:#a6e22e">Mixgpt</span><span style="color:#f92672">.</span>send <span style="color:#e6db74">&#34;tell me a joke in 5 words&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;\&#34;Why did the tomato blush?\&#34;&#34;</span>
</span></span></code></pre></div><p>Cool, now we already got our own pocket ChatGPT that we can access through mix, that was pretty quick, wasn&rsquo;t it? Let&rsquo;s make it a bit more automatic and close the loop. To wait for user input, we can use <code>IO.gets</code>, let&rsquo;s use that in a new function to directly ask for the next message:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-elixir" data-lang="elixir"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> wait_for_input_and_send() <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">IO</span><span style="color:#f92672">.</span>gets(<span style="color:#e6db74">&#34;&gt; &#34;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">|&gt;</span> send
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">|&gt;</span> <span style="color:#a6e22e">IO</span><span style="color:#f92672">.</span>puts() <span style="color:#75715e"># print the message out to have it nicely formatted</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  wait_for_input_and_send() <span style="color:#75715e"># recurse</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">end</span>
</span></span></code></pre></div><p>Type <code>recompile</code> into the iex shell to recompile the project and try again:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-elixir" data-lang="elixir"><span style="display:flex;"><span>iex<span style="color:#f92672">&gt;</span> <span style="color:#a6e22e">Mixgpt</span><span style="color:#f92672">.</span>wait_for_input_and_send
</span></span><span style="display:flex;"><span><span style="color:#f92672">&gt;</span> <span style="color:#a6e22e">Hi</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Hello</span>, how can <span style="color:#a6e22e">I</span> assist you today?
</span></span><span style="display:flex;"><span><span style="color:#f92672">&gt;</span> <span style="color:#a6e22e">Do</span> you like <span style="color:#a6e22e">Elixir</span><span style="color:#960050;background-color:#1e0010">?</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">As</span> an <span style="color:#a6e22e">AI</span> language model, <span style="color:#a6e22e">I</span> don<span style="color:#e6db74">&#39;t have personal preferences or feelings. However, I can tell you that Elixir is a popular programming language among developers and has some unique features that make it stand out, such as its scalability, fault tolerance, and ease of use for distributed systems.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&gt; what was the first message I sent you?
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">The first message you sent me was &#34;hi&#34;.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&gt;
</span></span></span></code></pre></div><h2 id="level-2-distributed-chatgpt-service">Level 2: Distributed ChatGPT service</h2>
<p>The example above is easy to do in any programming language, but let&rsquo;s go one step further and do something cool that you can do beautifully in Elixir and Erlang, but not as easily in other programming languages: <strong>Let&rsquo;s turn this thing into a proper distributed system!</strong></p>
<p>As a reminder, our mini chatgpt is running inside a <code>server</code>! We did this because that&rsquo;s how state is handled in Elixir. Even though we created the interface so that we can easily just call <code>send(&quot;why is the sky green?&quot;)</code>, on the back it still sends a new message to this gpt-server, then waits until it responds. It just does this so transparently that we don&rsquo;t even notice.</p>
<p>But what&rsquo;s even cooler: GenServers can already be accessed from other nodes within same BEAM cluster, so we <em>already have</em> something that is usable over a network! Let&rsquo;s take a look at how to do that, with only a little bit of black magic.</p>
<p>Close the iex shell we had open, and create a new one, but this time with the <code>--name</code> parameter:</p>
<pre tabindex="0"><code>iex --name gptserver@127.0.0.1 -S mix
iex&gt; Mixgpt.start_link
{:ok,  #PID&lt;0.276.0&gt;}
</code></pre><p><code>--name</code> tells elixir to start a distributed node with the given name, on the given network. So we created a distributed node <code>gptserver</code> on the network <code>127.0.0.1</code></p>
<p>In a new terminal window, start another iex session, this time without <code>-S mix</code>, but still with <code>--name</code>:</p>
<pre tabindex="0"><code>iex --name client@127.0.0.1
</code></pre><p>To now execute things on another cluster node, Elixir provides the <code>Node.spawn_link</code> function among other things. With this you could do stuff like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-elixir" data-lang="elixir"><span style="display:flex;"><span>iex<span style="color:#f92672">&gt;</span> <span style="color:#a6e22e">Node</span><span style="color:#f92672">.</span>spawn_link(<span style="color:#e6db74">:&#34;gptserver@127.0.0.1&#34;</span>, <span style="color:#66d9ef">fn</span> <span style="color:#f92672">-&gt;</span> <span style="color:#a6e22e">IO</span><span style="color:#f92672">.</span>puts(<span style="color:#e6db74">&#34;hi there, I&#39;m running on another node&#34;</span>) <span style="color:#66d9ef">end</span>)
</span></span></code></pre></div><p>But we already have a GenServer running, so no need for that and just hit the GenServer directly. This is also very easy to do, just by changing the <code>GenServer.call(:gptserver, {:msg, msg})</code> call to also include the node information where <code>:gptserver</code> is running: <code>{:gptserver, :&quot;gptserver@127.0.0.1&quot;}</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-elixir" data-lang="elixir"><span style="display:flex;"><span>iex<span style="color:#f92672">&gt;</span> <span style="color:#a6e22e">GenServer</span><span style="color:#f92672">.</span>call({<span style="color:#e6db74">:gptserver</span>, <span style="color:#e6db74">:&#34;gptserver@127.0.0.1&#34;</span>}, {<span style="color:#e6db74">:msg</span>, <span style="color:#e6db74">&#34;Helloo, anyone there?&#34;</span>})
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;Hello! Yes, I&#39;m here. How can I assist you?&#34;</span>
</span></span></code></pre></div><p>Do you realize what just happened? Our completely separate <code>iex</code> shells just connected to each other to send a message to the GenServer running under the <code>:gptserver</code> name, which then hits the OpenAI API, returns the data, and relays it back to the other <code>iex</code> shell!</p>
<p>We just created a distributed ChatGPT service on our local network (well, localhost), all without any extra setup required! This even works across machines, as long as the nodes are running reachable on the same network :)</p>
<h2 id="final-step-a-global-shell-command">Final step: A global shell command</h2>
<p>You can already guess what comes next. We can run Elixir code directly with <code>elixir -e &quot;IO.puts \&quot;Hi\&quot;&quot;</code>. The last lego piece missing is hooking this up to a global shell command, so that we can access our mini-ChatGPT whenever we need through a terminal, while retaining previous context.</p>
<p>To package it all up into a one-liner:</p>
<pre tabindex="0"><code>$ elixir --name client@127.0.0.1 -e &#34;GenServer.call({:gptserver, :&#39;gptserver@127.0.0.1&#39;}, {:msg, \&#34;hi\&#34;}) |&gt; IO.puts&#34;
Hello! How can I assist you today?
</code></pre><p>That&rsquo;s still the same call as before, except we&rsquo;re running it through <code>elixir -e &quot;&quot;</code>, and pipe the result into <code>IO.puts</code> to write it to the shell. This is still using the same GenServer that&rsquo;s still running (unless you shut it down), and it still retains the same message history from earlier.</p>
<p>Lastly, package into a bash function:</p>
<pre tabindex="0"><code>ai() { elixir --name client@127.0.0.1 -e &#34;GenServer.call({:gptserver, :&#39;gptserver@127.0.0.1&#39;}, {:msg, Enum.join(System.argv, \&#34; \&#34;)}) |&gt; IO.puts&#34;; $@;  };
</code></pre><p>Tadaa! We now have a system-wide (or network-wide) mini-ChatGPT that is fully <strong>stateful</strong></p>
<pre tabindex="0"><code>$ ai is the sky blue?
Yes, on clear days, the daytime sky typically appears blue due to the scattering of sunlight by Earth&#39;s atmosphere.
$ ai what is the last message I sent you?
Your last message was &#34;is the sky blue?&#34;
</code></pre><p>We could easily put this on a home server and have it available across all of our machines without any extra setup if we really wanted to</p>
<p>Of course, this is still super barebones and in reality, we&rsquo;d do a few more things:</p>
<ul>
<li>A Supervisor to handle crashes</li>
<li>Changing <code>:gptserver</code> to be global, so we don&rsquo;t need to know the hostname</li>
<li>Some way to reset, or clear the message history to not have messages endlessly pile up</li>
<li>Better error handling</li>
</ul>
<p>among others</p>
<p><a href="https://github.com/dvcrn/elixir-cli-chatgpt">Check out the code for this on GitHub</a></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>On ChatGPT, GPT3 and Dall-E in my day-to-day life</title>
      <link>https://david.coffee/gpt3-dalle-chatgpt-how-i-use-ai-today/</link>
      <pubDate>Thu, 16 Feb 2023 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/gpt3-dalle-chatgpt-how-i-use-ai-today/</guid><media:content url="https://david.coffee/dalle-cat-riding-teaser.png" medium="image" />
          <enclosure url="https://david.coffee/dalle-cat-riding-teaser.png" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/dalle-cat-riding-teaser.png&#34; /&gt;&lt;/p&gt;&lt;p&gt;You probably know what ChatGPT and Dall E are by now - AI exploded into mainstream with the recent releases (most notably ChatGPT) out of nowhere, and now even Bing out of all things is becoming the latest AI craze.&lt;/p&gt;
&lt;p&gt;As someone doing a lot of solo work, I became very grateful of the value tools like ChatGPT, GPT3 and DallE bring, and successfullly integrated them into my day to day work, both personal but also profesionally. Even for someone working in IT heavily, I&amp;rsquo;m still amazed by how damn good these tools actually are.&lt;/p&gt;
&lt;p&gt;Today I want to touch on my most common usecases.&lt;/p&gt;
&lt;h2 id=&#34;dall-e-my-graphic-design-buddy&#34;&gt;Dall-E: My Graphic Design buddy&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s start with Dall-E. Dall-E, also known as &amp;ldquo;Dall E 2,&amp;rdquo; is a generative text-to-image model. There are other popular ones like MidJourney or Stable Diffusion, but I prefer the results I get from Dall-E. If you haven&amp;rsquo;t heard of it, let me give you a quick rundown: these tools allow you to generate images by describing what you want to see in the image. For example, if you input &amp;ldquo;a cat riding a rocket across the stars, oil painting,&amp;rdquo; well, you&amp;rsquo;ll get an image like this:&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./dalle-cat-riding.png&#34; alt=&#34;DALL·E 2023-02-16 22.53.03 - a cat riding a rocket across the stars, oil painting&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve been following this blog, you may have noticed that most of the recent images I&amp;rsquo;ve published were generated with Dall-E. I like to have a header teaser image for each post to add consistency to my article format. Before Dall-E, I used to take screenshots of CLIs, code, or find something fitting from Unsplash, but that was too generic and boring for my taste. I didn&amp;rsquo;t want my posts to give off the &amp;ldquo;guru vibe&amp;rdquo; that you often see on Medium.&lt;/p&gt;
&lt;p&gt;But my use of Dall-E goes beyond just that. Just recently, I was working on an app and needed a logo to publish it in the AppStore. I&amp;rsquo;m no designer, and my previous options were either to try to create something in Sketch, which could involve finding finished svg images and modifying them, or to go on Fiverr and find a designer. The problem with the latter was that I had no clear idea of what the logo should look like, and constantly saying &amp;ldquo;ah no, that&amp;rsquo;s not what I wanted&amp;rdquo; was awkward and annoying.&lt;/p&gt;
&lt;p&gt;With Dall-E, I can explore different ideas completely on my own, quickly try out different concepts, and get a feel for whether the idea in my head even makes sense on a canvas. What I had in mind for this was something along the lines of &amp;ldquo;an envelope wearing glasses&amp;rdquo;, and the result:&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./maskedemaillogos.png&#34; alt=&#34;maskedemaillogos&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Dall-E unblocked me at a phase that would have taken days to weeks previously and allowed me to focus on more important things. I&amp;rsquo;m not saying that all the images it generates are great and ready for production, but having a temporary, good-looking image or a starting point when talking to a real designer is such a big timesaver for solo developers.&lt;/p&gt;
&lt;h2 id=&#34;chatgpt-and-gpt3&#34;&gt;ChatGPT and GPT3&lt;/h2&gt;
&lt;p&gt;Just when I thought Dall-E was going to be the most amazing thing I&amp;rsquo;ve seen in tech in years, ChatGPT and GPT3 came along. GPT3 was already impressive when it first came out, but API access was restricted by OpenAI and I couldn&amp;rsquo;t play with it properly. ChatGPT, with its free access, changed that.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&amp;ldquo;ChatGPT? GPT3? Wtf am I talking about?&amp;rdquo;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;GPT3, or &amp;ldquo;Generative Pretrained Transformer&amp;rdquo; is the third version of OpenAI&amp;rsquo;s generative large language text model. In short, the GPTs are models that are trained on huge amounts of text from various sources with the goal of allowing the mathematical neural network to &amp;ldquo;understand&amp;rdquo; written language, its structure, nuances, and how humans use it. This pre-trained knowledge is then used to predict with high mathematical likelihood how a sentence should continue.&lt;/p&gt;
&lt;p&gt;For example, if I were to write &amp;ldquo;The apple is,&amp;rdquo; and let the model complete it, it would have different paths to choose from, such as &amp;ldquo;red,&amp;rdquo; &amp;ldquo;tasty,&amp;rdquo; or &amp;ldquo;sweet.&amp;rdquo; Each possible path has weights assigned, and the model&amp;rsquo;s mathematical &amp;ldquo;brain&amp;rdquo; figures out the best path to take based on the data it was trained on.&lt;/p&gt;
&lt;p&gt;ChatGPT, on the other hand, is GPT3 (or GPT3.5?) that has been specifically tuned for conversational use, whereas GPT3 is more general-purpose. Although they are different, ChatGPT still uses the GPT3 (or &amp;ldquo;davinci&amp;rdquo;) model.&lt;/p&gt;
&lt;h3 id=&#34;gpt3-or-chatgpt&#34;&gt;GPT3 or ChatGPT?&lt;/h3&gt;
&lt;p&gt;Both tools have their place - ChatGPT is conversational and more general-purpose, while GPT3 is better for specific tasks. ChatGPT talks more, but GPT3 is harder to control with more options and parameters. I use ChatGPT when I want to try out ideas, similar to a sketchbook, and then GPT3 when I already know what I want and need to turn it into a repeatable task.&lt;/p&gt;
&lt;p&gt;Here are some of my use cases:&lt;/p&gt;
&lt;h4 id=&#34;text-rephrasing-handling-emotions-adding-professionalism&#34;&gt;Text Rephrasing (Handling Emotions, Adding Professionalism)&lt;/h4&gt;
&lt;p&gt;For example, when dealing with customer support, it can be challenging to maintain a professional tone, and emotions sometimes creep into my emails. ChatGPT takes those emotions out and rephrases my text to be professional.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./chatgpt-customer-support.png&#34; alt=&#34;chatgpt-customer-support&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;In fact, using ChatGPT for a proofreading pass has become the most common task I use it for! I can write what I want to say, then use ChatGPT to dial emotions in and out, make things more concise or add fluff (if I wanted to).&lt;/p&gt;
&lt;h4 id=&#34;language-learning-it-speaks-japanese&#34;&gt;Language Learning (it speaks Japanese!)&lt;/h4&gt;
&lt;p&gt;ChatGPT is surprisingly good for language learning. I use a lot of Japanese at work, and there are always moments when I&amp;rsquo;m not sure if the grammar I&amp;rsquo;m using is correct. I used to have no real way of figuring those mistakes out on my own and would have to rely on someone pointing them out (which in reality never happens).&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./chatgpt-language-teacher.png&#34; alt=&#34;chatgpt-language-teacher&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;ChatGPT isn&amp;rsquo;t perfect, but it&amp;rsquo;s catching the major mistakes and pointing me in the correct direction. I can&amp;rsquo;t recommend it to people learning English enough!&lt;/p&gt;
&lt;p&gt;Here is the prompt I am using for Japanese these days:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;You are a Japanese grammar correction bot. Do not chat, instead for each Japanese message, first output a corrected version of that message that is grammatically correct and could have been written by a Japanese native person. Do not forget particles and the correct tenses. Put this under a &amp;#34;corrected text&amp;#34; section.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Next, point out all the grammar mistakes that you have corrected in my text, and the reason why you corrected them. You are allowed to use Japanese words in the explanation. This will start after my next message. Reply with “ok” on this message if you understood what I want.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id=&#34;text-summarizing&#34;&gt;Text Summarizing&lt;/h4&gt;
&lt;p&gt;For when I want to save time and get a concise summary of something, like an article or a very long Slack message. Let&amp;rsquo;s take the article &lt;a href=&#34;https://www.danmcquillan.org/chatgpt.html&#34;&gt;We came to bury ChatGPT, not to praise it&lt;/a&gt; that was recently on the Hackernews landingpage and feed it into ChatGPT:&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./chatgpt-text-summary-1.png&#34; alt=&#34;chatgpt-text-summary-1&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;What we get with this prompt is a neatly summarized version that&amp;rsquo;s easy to digest.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./chatgpt-text-summary-2.png&#34; alt=&#34;chatgpt-text-summary-2&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;I can also adjust the level of technical detail in the summaries and how much professional lingo I want to allow through. Because ChatGPT is conversational, it makes these back and forth adjustements easy:&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./chatgpt-text-eli5.png&#34; alt=&#34;chatgpt-text-eli5&#34; /&gt;

&lt;/p&gt;
&lt;h4 id=&#34;brainstorming-names-and-ideas&#34;&gt;Brainstorming names and ideas&lt;/h4&gt;
&lt;p&gt;From app names to bouncing ideas back and forth, ChatGPT is great for brainstorming as well!&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./chatgpt-app-names.png&#34; alt=&#34;chatgpt-app-names&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Once I have some interesting ideas to go with, I can ask ChatGPT to iterate on names I like, or even add certain flair (make it sound more &amp;ldquo;Japanese&amp;rdquo;). Sadly it goes out of control very fast and for this specific usecase I usually have to reset the conversation a couple times before coming up with something good. (Great example why conversational and putting so much focus on the current context isn&amp;rsquo;t always the way to go, this is where GPT3 shines).&lt;/p&gt;
&lt;p&gt;It also tends to get hung up on specifics, for example when I wanted to brainstorm some name ideas for my tech freelance business, it really wanted to insert the word &amp;ldquo;tech&amp;rdquo; whereever possible.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./image.png&#34; alt=&#34;Some &amp;ldquo;tech names&amp;rdquo;&#34; /&gt;

&lt;/p&gt;
&lt;h2 id=&#34;the-future-is-going-to-be-interesting&#34;&gt;The future is going to be interesting&lt;/h2&gt;
&lt;p&gt;Together with Codex/Github Copilot, AI powered tools are becoming more and more of a staple in my day to day work. It&amp;rsquo;s incredible what we can already do with the tools we have today, and it&amp;rsquo;s going to be even more interesting to see what the tools of tomorrow will be able to do.&lt;/p&gt;
&lt;p&gt;But if I were to ramble a bit more, I do wonder if AI will make us a lot more lazy. Because why bother writing a longform article when some variant of GPT trained on the content of my blog can just generate it on the spot? Or why bother reading something when I can get a summary with as much detail as I want in seconds? How do you know the article you&amp;rsquo;re reading right now hasn&amp;rsquo;t just been generated for the sake of putting out &lt;em&gt;more&lt;/em&gt; &lt;em&gt;content&lt;/em&gt; for this blog?&lt;/p&gt;
&lt;p&gt;GPT3 can already generate better messages than I can in a fraction of the time. Dall E can design better than me, and Codex may soon be able to program more efficient and with less errors than me.&lt;/p&gt;
&lt;p&gt;While I am excited, I still feel that a lot of individuality is already getting lost. Maybe in a short while, the internet will be full of autogenerated &lt;em&gt;stuff&lt;/em&gt; - from messages that are rewritten by an AI before sending, to images and logos that got generated with prompt engineering.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/dalle-cat-riding-teaser.png" /></p><p>You probably know what ChatGPT and Dall E are by now - AI exploded into mainstream with the recent releases (most notably ChatGPT) out of nowhere, and now even Bing out of all things is becoming the latest AI craze.</p>
<p>As someone doing a lot of solo work, I became very grateful of the value tools like ChatGPT, GPT3 and DallE bring, and successfullly integrated them into my day to day work, both personal but also profesionally. Even for someone working in IT heavily, I&rsquo;m still amazed by how damn good these tools actually are.</p>
<p>Today I want to touch on my most common usecases.</p>
<h2 id="dall-e-my-graphic-design-buddy">Dall-E: My Graphic Design buddy</h2>
<p>Let&rsquo;s start with Dall-E. Dall-E, also known as &ldquo;Dall E 2,&rdquo; is a generative text-to-image model. There are other popular ones like MidJourney or Stable Diffusion, but I prefer the results I get from Dall-E. If you haven&rsquo;t heard of it, let me give you a quick rundown: these tools allow you to generate images by describing what you want to see in the image. For example, if you input &ldquo;a cat riding a rocket across the stars, oil painting,&rdquo; well, you&rsquo;ll get an image like this:</p>
<p>
<img class="img-fluid" src="./dalle-cat-riding.png" alt="DALL·E 2023-02-16 22.53.03 - a cat riding a rocket across the stars, oil painting" />

</p>
<p>If you&rsquo;ve been following this blog, you may have noticed that most of the recent images I&rsquo;ve published were generated with Dall-E. I like to have a header teaser image for each post to add consistency to my article format. Before Dall-E, I used to take screenshots of CLIs, code, or find something fitting from Unsplash, but that was too generic and boring for my taste. I didn&rsquo;t want my posts to give off the &ldquo;guru vibe&rdquo; that you often see on Medium.</p>
<p>But my use of Dall-E goes beyond just that. Just recently, I was working on an app and needed a logo to publish it in the AppStore. I&rsquo;m no designer, and my previous options were either to try to create something in Sketch, which could involve finding finished svg images and modifying them, or to go on Fiverr and find a designer. The problem with the latter was that I had no clear idea of what the logo should look like, and constantly saying &ldquo;ah no, that&rsquo;s not what I wanted&rdquo; was awkward and annoying.</p>
<p>With Dall-E, I can explore different ideas completely on my own, quickly try out different concepts, and get a feel for whether the idea in my head even makes sense on a canvas. What I had in mind for this was something along the lines of &ldquo;an envelope wearing glasses&rdquo;, and the result:</p>
<p>
<img class="img-fluid" src="./maskedemaillogos.png" alt="maskedemaillogos" />

</p>
<p>Dall-E unblocked me at a phase that would have taken days to weeks previously and allowed me to focus on more important things. I&rsquo;m not saying that all the images it generates are great and ready for production, but having a temporary, good-looking image or a starting point when talking to a real designer is such a big timesaver for solo developers.</p>
<h2 id="chatgpt-and-gpt3">ChatGPT and GPT3</h2>
<p>Just when I thought Dall-E was going to be the most amazing thing I&rsquo;ve seen in tech in years, ChatGPT and GPT3 came along. GPT3 was already impressive when it first came out, but API access was restricted by OpenAI and I couldn&rsquo;t play with it properly. ChatGPT, with its free access, changed that.</p>
<p><em>&ldquo;ChatGPT? GPT3? Wtf am I talking about?&rdquo;</em></p>
<p>GPT3, or &ldquo;Generative Pretrained Transformer&rdquo; is the third version of OpenAI&rsquo;s generative large language text model. In short, the GPTs are models that are trained on huge amounts of text from various sources with the goal of allowing the mathematical neural network to &ldquo;understand&rdquo; written language, its structure, nuances, and how humans use it. This pre-trained knowledge is then used to predict with high mathematical likelihood how a sentence should continue.</p>
<p>For example, if I were to write &ldquo;The apple is,&rdquo; and let the model complete it, it would have different paths to choose from, such as &ldquo;red,&rdquo; &ldquo;tasty,&rdquo; or &ldquo;sweet.&rdquo; Each possible path has weights assigned, and the model&rsquo;s mathematical &ldquo;brain&rdquo; figures out the best path to take based on the data it was trained on.</p>
<p>ChatGPT, on the other hand, is GPT3 (or GPT3.5?) that has been specifically tuned for conversational use, whereas GPT3 is more general-purpose. Although they are different, ChatGPT still uses the GPT3 (or &ldquo;davinci&rdquo;) model.</p>
<h3 id="gpt3-or-chatgpt">GPT3 or ChatGPT?</h3>
<p>Both tools have their place - ChatGPT is conversational and more general-purpose, while GPT3 is better for specific tasks. ChatGPT talks more, but GPT3 is harder to control with more options and parameters. I use ChatGPT when I want to try out ideas, similar to a sketchbook, and then GPT3 when I already know what I want and need to turn it into a repeatable task.</p>
<p>Here are some of my use cases:</p>
<h4 id="text-rephrasing-handling-emotions-adding-professionalism">Text Rephrasing (Handling Emotions, Adding Professionalism)</h4>
<p>For example, when dealing with customer support, it can be challenging to maintain a professional tone, and emotions sometimes creep into my emails. ChatGPT takes those emotions out and rephrases my text to be professional.</p>
<p>
<img class="img-fluid" src="./chatgpt-customer-support.png" alt="chatgpt-customer-support" />

</p>
<p>In fact, using ChatGPT for a proofreading pass has become the most common task I use it for! I can write what I want to say, then use ChatGPT to dial emotions in and out, make things more concise or add fluff (if I wanted to).</p>
<h4 id="language-learning-it-speaks-japanese">Language Learning (it speaks Japanese!)</h4>
<p>ChatGPT is surprisingly good for language learning. I use a lot of Japanese at work, and there are always moments when I&rsquo;m not sure if the grammar I&rsquo;m using is correct. I used to have no real way of figuring those mistakes out on my own and would have to rely on someone pointing them out (which in reality never happens).</p>
<p>
<img class="img-fluid" src="./chatgpt-language-teacher.png" alt="chatgpt-language-teacher" />

</p>
<p>ChatGPT isn&rsquo;t perfect, but it&rsquo;s catching the major mistakes and pointing me in the correct direction. I can&rsquo;t recommend it to people learning English enough!</p>
<p>Here is the prompt I am using for Japanese these days:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>You are a Japanese grammar correction bot. Do not chat, instead for each Japanese message, first output a corrected version of that message that is grammatically correct and could have been written by a Japanese native person. Do not forget particles and the correct tenses. Put this under a &#34;corrected text&#34; section.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Next, point out all the grammar mistakes that you have corrected in my text, and the reason why you corrected them. You are allowed to use Japanese words in the explanation. This will start after my next message. Reply with “ok” on this message if you understood what I want.
</span></span></code></pre></div><h4 id="text-summarizing">Text Summarizing</h4>
<p>For when I want to save time and get a concise summary of something, like an article or a very long Slack message. Let&rsquo;s take the article <a href="https://www.danmcquillan.org/chatgpt.html">We came to bury ChatGPT, not to praise it</a> that was recently on the Hackernews landingpage and feed it into ChatGPT:</p>
<p>
<img class="img-fluid" src="./chatgpt-text-summary-1.png" alt="chatgpt-text-summary-1" />

</p>
<p>What we get with this prompt is a neatly summarized version that&rsquo;s easy to digest.</p>
<p>
<img class="img-fluid" src="./chatgpt-text-summary-2.png" alt="chatgpt-text-summary-2" />

</p>
<p>I can also adjust the level of technical detail in the summaries and how much professional lingo I want to allow through. Because ChatGPT is conversational, it makes these back and forth adjustements easy:</p>
<p>
<img class="img-fluid" src="./chatgpt-text-eli5.png" alt="chatgpt-text-eli5" />

</p>
<h4 id="brainstorming-names-and-ideas">Brainstorming names and ideas</h4>
<p>From app names to bouncing ideas back and forth, ChatGPT is great for brainstorming as well!</p>
<p>
<img class="img-fluid" src="./chatgpt-app-names.png" alt="chatgpt-app-names" />

</p>
<p>Once I have some interesting ideas to go with, I can ask ChatGPT to iterate on names I like, or even add certain flair (make it sound more &ldquo;Japanese&rdquo;). Sadly it goes out of control very fast and for this specific usecase I usually have to reset the conversation a couple times before coming up with something good. (Great example why conversational and putting so much focus on the current context isn&rsquo;t always the way to go, this is where GPT3 shines).</p>
<p>It also tends to get hung up on specifics, for example when I wanted to brainstorm some name ideas for my tech freelance business, it really wanted to insert the word &ldquo;tech&rdquo; whereever possible.</p>
<p>
<img class="img-fluid" src="./image.png" alt="Some &ldquo;tech names&rdquo;" />

</p>
<h2 id="the-future-is-going-to-be-interesting">The future is going to be interesting</h2>
<p>Together with Codex/Github Copilot, AI powered tools are becoming more and more of a staple in my day to day work. It&rsquo;s incredible what we can already do with the tools we have today, and it&rsquo;s going to be even more interesting to see what the tools of tomorrow will be able to do.</p>
<p>But if I were to ramble a bit more, I do wonder if AI will make us a lot more lazy. Because why bother writing a longform article when some variant of GPT trained on the content of my blog can just generate it on the spot? Or why bother reading something when I can get a summary with as much detail as I want in seconds? How do you know the article you&rsquo;re reading right now hasn&rsquo;t just been generated for the sake of putting out <em>more</em> <em>content</em> for this blog?</p>
<p>GPT3 can already generate better messages than I can in a fraction of the time. Dall E can design better than me, and Codex may soon be able to program more efficient and with less errors than me.</p>
<p>While I am excited, I still feel that a lot of individuality is already getting lost. Maybe in a short while, the internet will be full of autogenerated <em>stuff</em> - from messages that are rewritten by an AI before sending, to images and logos that got generated with prompt engineering.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Refactoring code with ChatGPT</title>
      <link>https://david.coffee/refactoring-code-with-chat-gpt/</link>
      <pubDate>Sun, 04 Dec 2022 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/refactoring-code-with-chat-gpt/</guid><media:content url="https://david.coffee/refactoring-code-with-chat-gpt/robot-in-front-of-screen.png" medium="image" />
          <enclosure url="https://david.coffee/refactoring-code-with-chat-gpt/robot-in-front-of-screen.png" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/refactoring-code-with-chat-gpt/robot-in-front-of-screen.png&#34; /&gt;&lt;/p&gt;&lt;p&gt;ChatGPT is really damn impressive and the more I read about it, the more it feels like withcraft. I see people using it for game development, coming up with titles, improving prompts for MidJourney and and and. It&amp;rsquo;s shaping up to be one of those universally useful tools that are great to keep around.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s one thing I had on my mind: Can it help me when I code?&lt;/p&gt;
&lt;p&gt;I was working on a little pet project and had some pretty inefficient code I slapped together for a middleware while watching TV:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;package&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;middleware&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;context&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;encoding/json&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;net/http&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;parsedJsonBodyContextKey&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt;{}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ParseJsonBody&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Handler&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Handler&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;HandlerFunc&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ResponseWriter&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Request&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;json&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NewDecoder&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Body&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;Decode&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;context&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithValue&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Context&lt;/span&gt;(), &lt;span style=&#34;color:#a6e22e&#34;&gt;parsedJsonBodyContextKey&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ServeHTTP&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithContext&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;context&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithValue&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Context&lt;/span&gt;(), &lt;span style=&#34;color:#a6e22e&#34;&gt;parsedJsonBodyContextKey&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ServeHTTP&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithContext&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The code takes whatever is passed into the request body, tries to JSON decode it, then puts the result into context. If no body exists or isn&amp;rsquo;t JSON, it will pass &lt;code&gt;nil&lt;/code&gt; instead.&lt;/p&gt;
&lt;p&gt;I wanted to shorten and make it a bit more efficient but though it&amp;rsquo;d be a cool experiment to let ChatGPT do it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Me: Can you refactor the following code to make it more efficient?&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;code from above here&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;ChatGPT: Here are a few ways the provided Go code can be imporved:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;ParseJsonBody()&lt;/code&gt; function defines a &lt;code&gt;data&lt;/code&gt; variable to store the parsed JSON body, but it doesn&amp;rsquo;t appear to be used anywhere in the function. This variable can be removed.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ParseJsonBody()&lt;/code&gt; function creates a new &lt;code&gt;context.Context&lt;/code&gt; with a key-value pair for the parsed JSON body, but it sets the value to &lt;code&gt;nil&lt;/code&gt; in both branches of the &lt;code&gt;if&lt;/code&gt; statement. This means that the value of the parsed JSON body will always be &lt;code&gt;nil&lt;/code&gt;, regardless of whether the JSON body was parsed successfully or not. The value of the parsed JSON body should be set to the parsed JSON data if parsing was successful, and &lt;code&gt;nil&lt;/code&gt; if parsing failed.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ParseJsonBody()&lt;/code&gt; function passes the original &lt;code&gt;http.Request&lt;/code&gt; object to the &lt;code&gt;next&lt;/code&gt; handler, even if the JSON body was parsed successfully. In this case, the &lt;code&gt;http.Request&lt;/code&gt; object should be updated with the new &lt;code&gt;context.Context&lt;/code&gt; that contains the parsed JSON body before being passed to the &lt;code&gt;next&lt;/code&gt; handler.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here is how the code can be improved:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;package&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;middleware&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;context&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;encoding/json&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;net/http&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;parsedJsonBodyContextKey&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt;{}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ParseJsonBody&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Handler&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Handler&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;HandlerFunc&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ResponseWriter&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Request&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;json&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NewDecoder&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Body&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;Decode&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;context&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithValue&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Context&lt;/span&gt;(), &lt;span style=&#34;color:#a6e22e&#34;&gt;parsedJsonBodyContextKey&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ServeHTTP&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithContext&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;context&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithValue&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Context&lt;/span&gt;(), &lt;span style=&#34;color:#a6e22e&#34;&gt;parsedJsonBodyContextKey&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ServeHTTP&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithContext&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/blockquote&gt;
&lt;p&gt;Huh, turns out I copy pasted and forgot to change the normal branch to actually add &lt;code&gt;data&lt;/code&gt; into the context - whoops. ChatGPT spotted this, understood what this was supposed to do and fixed it.&lt;/p&gt;
&lt;p&gt;Can we make it more compact though?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Me: Can you make the code more compact and more DRY?&lt;/p&gt;
&lt;p&gt;ChatGPT: Yes, the provided code can be made more compact and DRY (Don&amp;rsquo;t Repeat Yourself) by removing the duplicate code and using a single &lt;code&gt;context.Context&lt;/code&gt; object.&lt;/p&gt;
&lt;p&gt;Here is how the code can be improved:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;package&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;middleware&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;context&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;encoding/json&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;net/http&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;parsedJsonBodyContextKey&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt;{}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ParseJsonBody&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Handler&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Handler&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;HandlerFunc&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ResponseWriter&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Request&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;json&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NewDecoder&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Body&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;Decode&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;context&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithValue&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Context&lt;/span&gt;(), &lt;span style=&#34;color:#a6e22e&#34;&gt;parsedJsonBodyContextKey&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ServeHTTP&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithContext&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/blockquote&gt;
&lt;p&gt;This is exactly what I had in mind for refactoring this code: No need for the if condition at all because &lt;code&gt;var data map[string]interface{}&lt;/code&gt; is &lt;code&gt;nil&lt;/code&gt; by default. Only nitpick would be that we assign &lt;code&gt;err&lt;/code&gt; and never use it, so the compiler will complain about this code.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s add a function to get the data out of the context again. Can ChatGPT summon this function for us?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Me: Please add a function that takes the body out of the context again&lt;/p&gt;
&lt;p&gt;ChatGPT: To extract the parsed JSON body from the &lt;code&gt;context.Context&lt;/code&gt; object, a new function can be added to the &lt;code&gt;middleware&lt;/code&gt; package. This function can take a &lt;code&gt;context.Context&lt;/code&gt; object as an argument and return the parsed JSON body, if it exists in the context, or &lt;code&gt;nil&lt;/code&gt; otherwise.&lt;/p&gt;
&lt;p&gt;Here is an example implementation of this function:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;package&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;middleware&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;context&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;encoding/json&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;parsedJsonBodyContextKey&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt;{}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ParseJsonBody&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Handler&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Handler&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;HandlerFunc&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ResponseWriter&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Request&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;json&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NewDecoder&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Body&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;Decode&lt;/span&gt;(&lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;context&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithValue&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Context&lt;/span&gt;(), &lt;span style=&#34;color:#a6e22e&#34;&gt;parsedJsonBodyContextKey&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;next&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ServeHTTP&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithContext&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;GetParsedJsonBody&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;context&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Context&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{} {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;val&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;parsedJsonBodyContextKey&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;val&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;val&lt;/span&gt;.(&lt;span style=&#34;color:#66d9ef&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To use this function, you can pass a &lt;code&gt;context.Context&lt;/code&gt; object as an argument and check the return value for &lt;code&gt;nil&lt;/code&gt; to determine whether the JSON body was parsed successfully or not.&lt;/p&gt;
&lt;p&gt;Here is an example of how this function can be used:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;package&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;context&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;fmt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;net/http&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;github.com/myproject/middleware&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;HandleFunc&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ResponseWriter&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Request&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;parsedBody&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;middleware&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;GetParsedJsonBody&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Context&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;parsedBody&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Write&lt;/span&gt;([]byte(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;JSON body was not parsed&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Write&lt;/span&gt;([]byte(&lt;span style=&#34;color:#a6e22e&#34;&gt;fmt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Sprintf&lt;/span&gt;(&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/blockquote&gt;
&lt;p&gt;Short and concise. It understood what I wanted it to do, it delivered a solution and even gave an explanation with examples how to use it!&lt;/p&gt;
&lt;p&gt;After a bit of hacking I noticed that using &lt;code&gt;map[string]interface{}&lt;/code&gt; is kind of a PITA because we have to do many type assertions and checks if keys exist. I got curious if we can tell it to swap out the type into something custom that has a nicer interface:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Me: Instead of using map[string]interface{}, let&amp;rsquo;s use a custom type that has methods for getting the value for a given key, or a default&lt;/p&gt;
&lt;p&gt;ChatGPT: To use a custom type that has methods for getting the value for a given key or a default value, you can define a new &lt;code&gt;ParsedJsonBody&lt;/code&gt; type that satisfies the &lt;code&gt;context.Context&lt;/code&gt; interface. This type can be a struct that contains a &lt;code&gt;map[string]interface{}&lt;/code&gt; field to store the parsed JSON body.&lt;/p&gt;
&lt;p&gt;Here is an example implementation of the &lt;code&gt;ParsedJsonBody&lt;/code&gt; type:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ParsedJsonBody&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ParsedJsonBody&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{}) &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{} {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;parsedJsonBodyContextKey&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ParsedJsonBody&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;Get&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{} {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ParsedJsonBody&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;GetString&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;val&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;val&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;val&lt;/span&gt;.(&lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ParsedJsonBody&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;GetInt&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;val&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;val&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;val&lt;/span&gt;.(&lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;ParseJsonBody()&lt;/code&gt; middleware function can be updated to use the &lt;code&gt;ParsedJsonBody&lt;/code&gt; type instead of &lt;code&gt;map[string]interface{}&lt;/code&gt;:&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Hmm okay, this is where things get a bit weird, but that could have also been my way of explaining what I wanted.&lt;/p&gt;
&lt;p&gt;The code &lt;em&gt;looks ok&lt;/em&gt; on first skimming, but it ignored my instruction to have a default. I think it interpreted that as &amp;ldquo;we&amp;rsquo;ll return a default&amp;rdquo;, but not one I can specify.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s lots of type assertions in there that don&amp;rsquo;t have any checks, so this will panic if we were to try to &lt;code&gt;GetInt&lt;/code&gt; something that isn&amp;rsquo;t an int. Also, &lt;em&gt;you can define a new &lt;code&gt;ParsedJsonBody&lt;/code&gt; type that satisfies the &lt;code&gt;context.Context&lt;/code&gt; interface&lt;/em&gt; is nonsense, and so is the &lt;code&gt;Value()&lt;/code&gt; method.&lt;/p&gt;
&lt;p&gt;What I actually wanted was a custom type like &lt;code&gt;type MyType map[string]interface{}&lt;/code&gt;, not a struct with a type field, but whatever.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s ask directly if it thinks anything is wrong with that code:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Me: Thanks for the code, do you see any problems with this implementation?&lt;/p&gt;
&lt;p&gt;ChatGPT: There is a potential problem with the provided implementation of the &lt;code&gt;ParsedJsonBody&lt;/code&gt; type. The &lt;code&gt;Value()&lt;/code&gt; method returns a pointer to the &lt;code&gt;ParsedJsonBody&lt;/code&gt; struct, but the &lt;code&gt;Get()&lt;/code&gt;, &lt;code&gt;GetString()&lt;/code&gt;, and &lt;code&gt;GetInt()&lt;/code&gt; methods return values of the underlying &lt;code&gt;data&lt;/code&gt; field. This means that if the &lt;code&gt;ParsedJsonBody&lt;/code&gt; struct is modified after it is added to the &lt;code&gt;context.Context&lt;/code&gt; object, the modifications will be reflected in the &lt;code&gt;context.Context&lt;/code&gt;, but not in the return values of the &lt;code&gt;Get()&lt;/code&gt;, &lt;code&gt;GetString()&lt;/code&gt;, and &lt;code&gt;GetInt()&lt;/code&gt; methods.&lt;/p&gt;
&lt;p&gt;To fix this problem, the &lt;code&gt;Get()&lt;/code&gt;, &lt;code&gt;GetString()&lt;/code&gt;, and &lt;code&gt;GetInt()&lt;/code&gt; methods should return copies of the values in the &lt;code&gt;data&lt;/code&gt; field, rather than the values themselves.&lt;/p&gt;
&lt;p&gt;Here is an updated implementation of the &lt;code&gt;ParsedJsonBody&lt;/code&gt; type that fixes this problem:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ParsedJsonBody&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ParsedJsonBody&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{}) &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{} {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;parsedJsonBodyContextKey&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ParsedJsonBody&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;Get&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{} {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;val&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; !&lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;val&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ParsedJsonBody&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;GetString&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;val&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt;].(&lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; !&lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;val&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ParsedJsonBody&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;GetInt&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;val&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt;].(&lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; !&lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;val&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/blockquote&gt;
&lt;p&gt;The good: It fixed the type assertions and now has a checks, neat!&lt;/p&gt;
&lt;p&gt;The bad: The explanation it gave on why it added those doesn&amp;rsquo;t make sense. It didn&amp;rsquo;t actually change those to &lt;em&gt;return copies of values&lt;/em&gt;. Let&amp;rsquo;s do one more optimization pass.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Me: Please further optimize this code&lt;/p&gt;
&lt;p&gt;ChatGPT: Here are a few ways the provided implementation of the &lt;code&gt;ParsedJsonBody&lt;/code&gt; type can be further optimized:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;Value()&lt;/code&gt; method currently returns &lt;code&gt;p&lt;/code&gt; if the &lt;code&gt;key&lt;/code&gt; argument matches &lt;code&gt;parsedJsonBodyContextKey&lt;/code&gt;, and &lt;code&gt;nil&lt;/code&gt; otherwise. Since &lt;code&gt;parsedJsonBodyContextKey&lt;/code&gt; is a global variable that is only used in the &lt;code&gt;ParseJsonBody()&lt;/code&gt; middleware function, this check is unnecessary. The &lt;code&gt;Value()&lt;/code&gt; method can simply return &lt;code&gt;p&lt;/code&gt; without checking the &lt;code&gt;key&lt;/code&gt; argument.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Get()&lt;/code&gt;, &lt;code&gt;GetString()&lt;/code&gt;, and &lt;code&gt;GetInt()&lt;/code&gt; methods currently check if the specified key exists in the &lt;code&gt;data&lt;/code&gt; field and return a default value if the key does not exist. This check can be removed, because the &lt;code&gt;data&lt;/code&gt; field is a &lt;code&gt;map[string]interface{}&lt;/code&gt; and trying to access a non-existent key in a map will return the zero value for the type of the map&amp;rsquo;s value (&lt;code&gt;interface{}&lt;/code&gt; in this case, which is &lt;code&gt;nil&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;GetString()&lt;/code&gt; and &lt;code&gt;GetInt()&lt;/code&gt; methods currently use the &lt;code&gt;ok&lt;/code&gt; return value of the type assertion to determine whether the value for the specified key is of the expected type. This check can be removed, because&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Me: Please show me an implementation with those points&lt;/p&gt;
&lt;p&gt;ChatGPT:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ParsedJsonBody&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ParsedJsonBody&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{}) &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{} {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#75715e&#34;&gt;// The key argument is not used, so we can simply return p&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ParsedJsonBody&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;Get&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{} {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#75715e&#34;&gt;// We don&amp;#39;t need to check if the key exists, because trying&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#75715e&#34;&gt;// to access a non-existent key in a map will return the zero&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#75715e&#34;&gt;// value for the type of the map&amp;#39;s value (nil in this case)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ParsedJsonBody&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;GetString&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#75715e&#34;&gt;// We don&amp;#39;t need to check the type of the value, because trying&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#75715e&#34;&gt;// to assert a non-string value to a string type will return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#75715e&#34;&gt;// the zero value for the string type (&amp;#34;&amp;#34; in this case)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt;].(&lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ParsedJsonBody&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;GetInt&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#75715e&#34;&gt;// We don&amp;#39;t need to check the type of the value, because trying&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#75715e&#34;&gt;// to assert a non-int value to an int type will return the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#75715e&#34;&gt;// zero value for the int type (0 in this case)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt;].(&lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/blockquote&gt;
&lt;p&gt;It got more compact for sure, but the code is wrong. It got rid of the type checks with the confident explanation that type-asserting &lt;em&gt;a non-int value to an int type will return the zero value for the int type&lt;/em&gt;, but this is not correct and will panic. Here&amp;rsquo;s the code in Golang playground to try out: &lt;a href=&#34;https://play.golang.com/p/8jjBJyrJ1uC&#34;&gt;https://play.golang.com/p/8jjBJyrJ1uC&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;While impressive, at this point it&amp;rsquo;s internal state of my program and what I&amp;rsquo;m trying to do is a bit too messed up, and the confidently wrong explanation+implementation can be dangerous. For a moment I self-doubted my own knowledge of Golang and had to try this out in the playground to make sure.&lt;/p&gt;
&lt;p&gt;The network isn&amp;rsquo;t specifically trained on code, so I guess it just doesn&amp;rsquo;t fully understand the inner-workings of Golang, without providing enough context to it first.&lt;/p&gt;
&lt;h2 id=&#34;generating-unit-tests&#34;&gt;Generating unit tests&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s do something fun - unit tests (&lt;em&gt;groans&lt;/em&gt;)! If it can spit out functional unit tests for stuff I&amp;rsquo;m too lazy to write, that would already be very helpful. After all, it seems to understand code and is able to give explanations for what&amp;rsquo;s going on.&lt;/p&gt;
&lt;p&gt;The code here is a snippet from a JWT token parser. It validates the token, extracts sid/uid scopes (UUID) and checks if those are actually valid UUIDs.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Me: Consider the following Golang code:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// other code here&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Client&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;validateToken&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;jwt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Token&lt;/span&gt;) (&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Token&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;jwt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Validate&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;); &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#75715e&#34;&gt;// parse sid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;sid&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Get&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sid&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; !&lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;domain&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ErrTokenInvalidSID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;sidUUID&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;uuid&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Parse&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;sid&lt;/span&gt;.(&lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;domain&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ErrTokenInvalidSID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#75715e&#34;&gt;// parse uid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;uid&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Get&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;uid&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; !&lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;domain&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ErrTokenInvalidUID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;uidUUID&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;uuid&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Parse&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;uid&lt;/span&gt;.(&lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;domain&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ErrTokenInvalidUID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Token&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;SessionID&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;sidUUID&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;UserID&lt;/span&gt;:    &lt;span style=&#34;color:#a6e22e&#34;&gt;uidUUID&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}, &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// other code here&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Please generate some testcases for when sid or uid are invalid&lt;/p&gt;
&lt;p&gt;ChatGPT: Here are some test cases for when the &lt;code&gt;sid&lt;/code&gt; or &lt;code&gt;uid&lt;/code&gt; values in the token are invalid:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;TestValidateTokenInvalidSID&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;testing&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;T&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;// Create a new client&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;NewClient&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;some-secret-key&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;// Create a test token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;jwt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;New&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Set&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;uid&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;uuid&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;New&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;String&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;// Set the sid value to something invalid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Set&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sid&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;invalid-sid&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Set&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;jwt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ExpirationKey&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Now&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;Add&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Minute&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;Unix&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;// Sign the token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;signedToken&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;jwt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Sign&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;jwt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithKey&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;alg&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;hmacSecret&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Errorf&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;unexpected error signing token: %s&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;// Validate the token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;validateToken&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;signedToken&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;domain&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ErrTokenInvalidSID&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Errorf&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;expected error to be %s, got %s&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;domain&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ErrTokenInvalidSID&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;TestValidateTokenInvalidUID&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;testing&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;T&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;// Create a new client&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;NewClient&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;some-secret-key&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;// Create a test token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;jwt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;New&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;// Set the uid value to something invalid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Set&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;uid&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;invalid-uid&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Set&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sid&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;uuid&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;New&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;String&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Set&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;jwt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ExpirationKey&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Now&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;Add&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Minute&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;Unix&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;// Sign the token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;signedToken&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;jwt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Sign&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;jwt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithKey&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;alg&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;hmacSecret&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Errorf&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;unexpected error signing token: %s&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;// Validate the token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;client&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;validateToken&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;signedToken&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;domain&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ErrTokenInvalidUID&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Errorf&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;expected error to be %s, got %s&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;domain&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ErrTokenInvalidUID&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/blockquote&gt;
&lt;p&gt;Sweet as. Sadly it&amp;rsquo;s missing a bit of context here too, so I can&amp;rsquo;t copy-paste the test as is. &lt;code&gt;jwt.Sign()&lt;/code&gt; isn&amp;rsquo;t returning a &lt;code&gt;jwt.Token&lt;/code&gt;, but the jwt token in signed form, so it can&amp;rsquo;t get passed to &lt;code&gt;validateToken&lt;/code&gt;, but there&amp;rsquo;s no way that it could have known that, without feeding the the jwt package into it first. Besides that it looks good, and the tests run after fixing that!&lt;/p&gt;
&lt;p&gt;And to go full circle, now that we have more unit tests, let&amp;rsquo;s ask it to optimize my &lt;code&gt;validateToken&lt;/code&gt; function as well:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;One possible optimization for the &lt;code&gt;validateToken&lt;/code&gt; function would be to move the code that parses the &lt;code&gt;sid&lt;/code&gt; and &lt;code&gt;uid&lt;/code&gt; values into a separate function, and then call that function from &lt;code&gt;validateToken&lt;/code&gt; instead of duplicating the code. This would make the code easier to read and maintain, and would also avoid repeating the same error-checking code multiple times.&lt;/p&gt;
&lt;p&gt;Here is an example of how the optimized &lt;code&gt;validateToken&lt;/code&gt; function could look like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Client&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;validateToken&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;jwt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Token&lt;/span&gt;) (&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Token&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// Validate the token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;jwt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Validate&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;); &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// Parse the sid and uid values from the token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;sid&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;parseSID&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;uid&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;parseUID&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Token&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;SessionID&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;sid&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;UserID&lt;/span&gt;:    &lt;span style=&#34;color:#a6e22e&#34;&gt;uid&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }, &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Client&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;parseSID&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;jwt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Token&lt;/span&gt;) (&lt;span style=&#34;color:#a6e22e&#34;&gt;uuid&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;UUID&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// parse sid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;sid&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Get&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sid&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; !&lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;uuid&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;UUID&lt;/span&gt;{}, &lt;span style=&#34;color:#a6e22e&#34;&gt;domain&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ErrTokenInvalidSID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;sidUUID&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;uuid&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Parse&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;sid&lt;/span&gt;.(&lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;uuid&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;UUID&lt;/span&gt;{}, &lt;span style=&#34;color:#a6e22e&#34;&gt;domain&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ErrTokenInvalidSID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;sidUUID&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Client&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;parseUID&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;jwt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Token&lt;/span&gt;) (&lt;span style=&#34;color:#a6e22e&#34;&gt;uuid&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;UUID&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// parse uid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;uid&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Get&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;uid&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; !&lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;uuid&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;UUID&lt;/span&gt;{}, &lt;span style=&#34;color:#a6e22e&#34;&gt;domain&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ErrTokenInvalidUID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;uidUUID&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;uuid&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Parse&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;uid&lt;/span&gt;.(&lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;uuid&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;UUID&lt;/span&gt;{}, &lt;span style=&#34;color:#a6e22e&#34;&gt;domain&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ErrTokenInvalidUID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;uidUUID&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/blockquote&gt;
&lt;p&gt;I was hoping it&amp;rsquo;d catch the naked &lt;code&gt;.(string)&lt;/code&gt; type assertion, but code splitting is also nice :D&lt;/p&gt;
&lt;p&gt;Although this iteration doesn&amp;rsquo;t really &lt;em&gt;avoid repeating the same error-checking code multiple times&lt;/em&gt; like it claims.&lt;/p&gt;
&lt;h2 id=&#34;collaborative-hacking-with-an-ai-buddy&#34;&gt;Collaborative hacking with an AI buddy&lt;/h2&gt;
&lt;p&gt;I use Copilot quite a lot so I&amp;rsquo;m already used to having an AI pair-programming buddy, but I strongly believe that as these tools get smarter, they will drastically change the way how we as engineers work.&lt;/p&gt;
&lt;p&gt;While not there yet, AI will be able to generate more efficient code &lt;em&gt;faster than us&lt;/em&gt;, spot errors we&amp;rsquo;d miss, or flat-out generate entire applications based on spec input.&lt;/p&gt;
&lt;p&gt;When Stable Diffusion came out, some people integrated it into their photo editors to quickly generate content based on a prompt. I am sure we&amp;rsquo;ll see something like this, but for code, in no time. Imagine reading through some code, hitting an &amp;ldquo;AI&amp;rdquo; button, then ask it to refactor a routine, or give an explanation what that code does. The plugin will then send all the necessary context to the neural net and ask it to do the task.&lt;/p&gt;
&lt;p&gt;/EDIT: someone already did it: &lt;a href=&#34;https://twitter.com/marcelpociot/status/1599180144551526400&#34;&gt;https://twitter.com/marcelpociot/status/1599180144551526400&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The next generation of AI powered plugins could also do things like quickly provide feedback when it strongly believes a mistake has been made, or when it has a high confidence score for a completion / refactoring.&lt;/p&gt;
&lt;p&gt;I think that would be really dang cool!&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/refactoring-code-with-chat-gpt/robot-in-front-of-screen.png" /></p><p>ChatGPT is really damn impressive and the more I read about it, the more it feels like withcraft. I see people using it for game development, coming up with titles, improving prompts for MidJourney and and and. It&rsquo;s shaping up to be one of those universally useful tools that are great to keep around.</p>
<p>Here&rsquo;s one thing I had on my mind: Can it help me when I code?</p>
<p>I was working on a little pet project and had some pretty inefficient code I slapped together for a middleware while watching TV:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">middleware</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;context&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;encoding/json&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;net/http&#34;</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">parsedJsonBodyContextKey</span> <span style="color:#66d9ef">struct</span>{}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ParseJsonBody</span>(<span style="color:#a6e22e">next</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Handler</span>) <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Handler</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandlerFunc</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">data</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">interface</span>{}
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">NewDecoder</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Body</span>).<span style="color:#a6e22e">Decode</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">data</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithValue</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Context</span>(), <span style="color:#a6e22e">parsedJsonBodyContextKey</span>, <span style="color:#66d9ef">nil</span>)
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">next</span>.<span style="color:#a6e22e">ServeHTTP</span>(<span style="color:#a6e22e">w</span>, <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">WithContext</span>(<span style="color:#a6e22e">ctx</span>))
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithValue</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Context</span>(), <span style="color:#a6e22e">parsedJsonBodyContextKey</span>, <span style="color:#66d9ef">nil</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">next</span>.<span style="color:#a6e22e">ServeHTTP</span>(<span style="color:#a6e22e">w</span>, <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">WithContext</span>(<span style="color:#a6e22e">ctx</span>))
</span></span><span style="display:flex;"><span>	})
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The code takes whatever is passed into the request body, tries to JSON decode it, then puts the result into context. If no body exists or isn&rsquo;t JSON, it will pass <code>nil</code> instead.</p>
<p>I wanted to shorten and make it a bit more efficient but though it&rsquo;d be a cool experiment to let ChatGPT do it:</p>
<blockquote>
<p>Me: Can you refactor the following code to make it more efficient?</p>
<p><code>&lt;code from above here&gt;</code></p>
<p>ChatGPT: Here are a few ways the provided Go code can be imporved:</p>
<ol>
<li>The <code>ParseJsonBody()</code> function defines a <code>data</code> variable to store the parsed JSON body, but it doesn&rsquo;t appear to be used anywhere in the function. This variable can be removed.</li>
<li>The <code>ParseJsonBody()</code> function creates a new <code>context.Context</code> with a key-value pair for the parsed JSON body, but it sets the value to <code>nil</code> in both branches of the <code>if</code> statement. This means that the value of the parsed JSON body will always be <code>nil</code>, regardless of whether the JSON body was parsed successfully or not. The value of the parsed JSON body should be set to the parsed JSON data if parsing was successful, and <code>nil</code> if parsing failed.</li>
<li>The <code>ParseJsonBody()</code> function passes the original <code>http.Request</code> object to the <code>next</code> handler, even if the JSON body was parsed successfully. In this case, the <code>http.Request</code> object should be updated with the new <code>context.Context</code> that contains the parsed JSON body before being passed to the <code>next</code> handler.</li>
</ol>
<p>Here is how the code can be improved:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">middleware</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;context&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;encoding/json&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;net/http&#34;</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">parsedJsonBodyContextKey</span> <span style="color:#66d9ef">struct</span>{}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ParseJsonBody</span>(<span style="color:#a6e22e">next</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Handler</span>) <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Handler</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandlerFunc</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">data</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">interface</span>{}
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">NewDecoder</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Body</span>).<span style="color:#a6e22e">Decode</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">data</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithValue</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Context</span>(), <span style="color:#a6e22e">parsedJsonBodyContextKey</span>, <span style="color:#66d9ef">nil</span>)
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">next</span>.<span style="color:#a6e22e">ServeHTTP</span>(<span style="color:#a6e22e">w</span>, <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">WithContext</span>(<span style="color:#a6e22e">ctx</span>))
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithValue</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Context</span>(), <span style="color:#a6e22e">parsedJsonBodyContextKey</span>, <span style="color:#a6e22e">data</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">next</span>.<span style="color:#a6e22e">ServeHTTP</span>(<span style="color:#a6e22e">w</span>, <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">WithContext</span>(<span style="color:#a6e22e">ctx</span>))
</span></span><span style="display:flex;"><span>	})
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></blockquote>
<p>Huh, turns out I copy pasted and forgot to change the normal branch to actually add <code>data</code> into the context - whoops. ChatGPT spotted this, understood what this was supposed to do and fixed it.</p>
<p>Can we make it more compact though?</p>
<blockquote>
<p>Me: Can you make the code more compact and more DRY?</p>
<p>ChatGPT: Yes, the provided code can be made more compact and DRY (Don&rsquo;t Repeat Yourself) by removing the duplicate code and using a single <code>context.Context</code> object.</p>
<p>Here is how the code can be improved:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">middleware</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;context&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;encoding/json&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;net/http&#34;</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">parsedJsonBodyContextKey</span> <span style="color:#66d9ef">struct</span>{}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ParseJsonBody</span>(<span style="color:#a6e22e">next</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Handler</span>) <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Handler</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandlerFunc</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">data</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">interface</span>{}
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">NewDecoder</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Body</span>).<span style="color:#a6e22e">Decode</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">data</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithValue</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Context</span>(), <span style="color:#a6e22e">parsedJsonBodyContextKey</span>, <span style="color:#a6e22e">data</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">next</span>.<span style="color:#a6e22e">ServeHTTP</span>(<span style="color:#a6e22e">w</span>, <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">WithContext</span>(<span style="color:#a6e22e">ctx</span>))
</span></span><span style="display:flex;"><span>	})
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></blockquote>
<p>This is exactly what I had in mind for refactoring this code: No need for the if condition at all because <code>var data map[string]interface{}</code> is <code>nil</code> by default. Only nitpick would be that we assign <code>err</code> and never use it, so the compiler will complain about this code.</p>
<p>Let&rsquo;s add a function to get the data out of the context again. Can ChatGPT summon this function for us?</p>
<blockquote>
<p>Me: Please add a function that takes the body out of the context again</p>
<p>ChatGPT: To extract the parsed JSON body from the <code>context.Context</code> object, a new function can be added to the <code>middleware</code> package. This function can take a <code>context.Context</code> object as an argument and return the parsed JSON body, if it exists in the context, or <code>nil</code> otherwise.</p>
<p>Here is an example implementation of this function:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">middleware</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;context&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;encoding/json&#34;</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">parsedJsonBodyContextKey</span> <span style="color:#66d9ef">struct</span>{}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">ParseJsonBody</span>(<span style="color:#a6e22e">next</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Handler</span>) <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Handler</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandlerFunc</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">data</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">interface</span>{}
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">NewDecoder</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Body</span>).<span style="color:#a6e22e">Decode</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">data</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">ctx</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithValue</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Context</span>(), <span style="color:#a6e22e">parsedJsonBodyContextKey</span>, <span style="color:#a6e22e">data</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">next</span>.<span style="color:#a6e22e">ServeHTTP</span>(<span style="color:#a6e22e">w</span>, <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">WithContext</span>(<span style="color:#a6e22e">ctx</span>))
</span></span><span style="display:flex;"><span>	})
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">GetParsedJsonBody</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>) <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">interface</span>{} {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">val</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Value</span>(<span style="color:#a6e22e">parsedJsonBodyContextKey</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">val</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">val</span>.(<span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">interface</span>{})
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>To use this function, you can pass a <code>context.Context</code> object as an argument and check the return value for <code>nil</code> to determine whether the JSON body was parsed successfully or not.</p>
<p>Here is an example of how this function can be used:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;context&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;fmt&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;net/http&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;github.com/myproject/middleware&#34;</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">HandleFunc</span>(<span style="color:#e6db74">&#34;/&#34;</span>, <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">parsedBody</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">middleware</span>.<span style="color:#a6e22e">GetParsedJsonBody</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Context</span>())
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">parsedBody</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">Write</span>([]byte(<span style="color:#e6db74">&#34;JSON body was not parsed&#34;</span>))
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">Write</span>([]byte(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#960050;background-color:#1e0010">&#34;</span>
</span></span></code></pre></div></blockquote>
<p>Short and concise. It understood what I wanted it to do, it delivered a solution and even gave an explanation with examples how to use it!</p>
<p>After a bit of hacking I noticed that using <code>map[string]interface{}</code> is kind of a PITA because we have to do many type assertions and checks if keys exist. I got curious if we can tell it to swap out the type into something custom that has a nicer interface:</p>
<blockquote>
<p>Me: Instead of using map[string]interface{}, let&rsquo;s use a custom type that has methods for getting the value for a given key, or a default</p>
<p>ChatGPT: To use a custom type that has methods for getting the value for a given key or a default value, you can define a new <code>ParsedJsonBody</code> type that satisfies the <code>context.Context</code> interface. This type can be a struct that contains a <code>map[string]interface{}</code> field to store the parsed JSON body.</p>
<p>Here is an example implementation of the <code>ParsedJsonBody</code> type:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ParsedJsonBody</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">data</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">interface</span>{}
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">p</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ParsedJsonBody</span>) <span style="color:#a6e22e">Value</span>(<span style="color:#a6e22e">key</span> <span style="color:#66d9ef">interface</span>{}) <span style="color:#66d9ef">interface</span>{} {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">key</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">parsedJsonBodyContextKey</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">p</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">p</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ParsedJsonBody</span>) <span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">key</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">interface</span>{} {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">data</span>[<span style="color:#a6e22e">key</span>]
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">p</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ParsedJsonBody</span>) <span style="color:#a6e22e">GetString</span>(<span style="color:#a6e22e">key</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">string</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">val</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">data</span>[<span style="color:#a6e22e">key</span>]
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">val</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">val</span>.(<span style="color:#66d9ef">string</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">p</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ParsedJsonBody</span>) <span style="color:#a6e22e">GetInt</span>(<span style="color:#a6e22e">key</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">int</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">val</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">data</span>[<span style="color:#a6e22e">key</span>]
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">val</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">val</span>.(<span style="color:#66d9ef">int</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>ParseJsonBody()</code> middleware function can be updated to use the <code>ParsedJsonBody</code> type instead of <code>map[string]interface{}</code>:</p></blockquote>
<p>Hmm okay, this is where things get a bit weird, but that could have also been my way of explaining what I wanted.</p>
<p>The code <em>looks ok</em> on first skimming, but it ignored my instruction to have a default. I think it interpreted that as &ldquo;we&rsquo;ll return a default&rdquo;, but not one I can specify.</p>
<p>There&rsquo;s lots of type assertions in there that don&rsquo;t have any checks, so this will panic if we were to try to <code>GetInt</code> something that isn&rsquo;t an int. Also, <em>you can define a new <code>ParsedJsonBody</code> type that satisfies the <code>context.Context</code> interface</em> is nonsense, and so is the <code>Value()</code> method.</p>
<p>What I actually wanted was a custom type like <code>type MyType map[string]interface{}</code>, not a struct with a type field, but whatever.</p>
<p>Let&rsquo;s ask directly if it thinks anything is wrong with that code:</p>
<blockquote>
<p>Me: Thanks for the code, do you see any problems with this implementation?</p>
<p>ChatGPT: There is a potential problem with the provided implementation of the <code>ParsedJsonBody</code> type. The <code>Value()</code> method returns a pointer to the <code>ParsedJsonBody</code> struct, but the <code>Get()</code>, <code>GetString()</code>, and <code>GetInt()</code> methods return values of the underlying <code>data</code> field. This means that if the <code>ParsedJsonBody</code> struct is modified after it is added to the <code>context.Context</code> object, the modifications will be reflected in the <code>context.Context</code>, but not in the return values of the <code>Get()</code>, <code>GetString()</code>, and <code>GetInt()</code> methods.</p>
<p>To fix this problem, the <code>Get()</code>, <code>GetString()</code>, and <code>GetInt()</code> methods should return copies of the values in the <code>data</code> field, rather than the values themselves.</p>
<p>Here is an updated implementation of the <code>ParsedJsonBody</code> type that fixes this problem:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ParsedJsonBody</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">data</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">interface</span>{}
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">p</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ParsedJsonBody</span>) <span style="color:#a6e22e">Value</span>(<span style="color:#a6e22e">key</span> <span style="color:#66d9ef">interface</span>{}) <span style="color:#66d9ef">interface</span>{} {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">key</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">parsedJsonBodyContextKey</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">p</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">p</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ParsedJsonBody</span>) <span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">key</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">interface</span>{} {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">val</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">data</span>[<span style="color:#a6e22e">key</span>]
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">val</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">p</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ParsedJsonBody</span>) <span style="color:#a6e22e">GetString</span>(<span style="color:#a6e22e">key</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">string</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">val</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">data</span>[<span style="color:#a6e22e">key</span>].(<span style="color:#66d9ef">string</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">val</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">p</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ParsedJsonBody</span>) <span style="color:#a6e22e">GetInt</span>(<span style="color:#a6e22e">key</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">int</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">val</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">data</span>[<span style="color:#a6e22e">key</span>].(<span style="color:#66d9ef">int</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">val</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></blockquote>
<p>The good: It fixed the type assertions and now has a checks, neat!</p>
<p>The bad: The explanation it gave on why it added those doesn&rsquo;t make sense. It didn&rsquo;t actually change those to <em>return copies of values</em>. Let&rsquo;s do one more optimization pass.</p>
<blockquote>
<p>Me: Please further optimize this code</p>
<p>ChatGPT: Here are a few ways the provided implementation of the <code>ParsedJsonBody</code> type can be further optimized:</p>
<ol>
<li>The <code>Value()</code> method currently returns <code>p</code> if the <code>key</code> argument matches <code>parsedJsonBodyContextKey</code>, and <code>nil</code> otherwise. Since <code>parsedJsonBodyContextKey</code> is a global variable that is only used in the <code>ParseJsonBody()</code> middleware function, this check is unnecessary. The <code>Value()</code> method can simply return <code>p</code> without checking the <code>key</code> argument.</li>
<li>The <code>Get()</code>, <code>GetString()</code>, and <code>GetInt()</code> methods currently check if the specified key exists in the <code>data</code> field and return a default value if the key does not exist. This check can be removed, because the <code>data</code> field is a <code>map[string]interface{}</code> and trying to access a non-existent key in a map will return the zero value for the type of the map&rsquo;s value (<code>interface{}</code> in this case, which is <code>nil</code>).</li>
<li>The <code>GetString()</code> and <code>GetInt()</code> methods currently use the <code>ok</code> return value of the type assertion to determine whether the value for the specified key is of the expected type. This check can be removed, because</li>
</ol>
<p>Me: Please show me an implementation with those points</p>
<p>ChatGPT:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ParsedJsonBody</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">data</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">interface</span>{}
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">p</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ParsedJsonBody</span>) <span style="color:#a6e22e">Value</span>(<span style="color:#a6e22e">key</span> <span style="color:#66d9ef">interface</span>{}) <span style="color:#66d9ef">interface</span>{} {
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// The key argument is not used, so we can simply return p</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">p</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">p</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ParsedJsonBody</span>) <span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">key</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">interface</span>{} {
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// We don&#39;t need to check if the key exists, because trying</span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// to access a non-existent key in a map will return the zero</span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// value for the type of the map&#39;s value (nil in this case)</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">data</span>[<span style="color:#a6e22e">key</span>]
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">p</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ParsedJsonBody</span>) <span style="color:#a6e22e">GetString</span>(<span style="color:#a6e22e">key</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">string</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// We don&#39;t need to check the type of the value, because trying</span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// to assert a non-string value to a string type will return</span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// the zero value for the string type (&#34;&#34; in this case)</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">data</span>[<span style="color:#a6e22e">key</span>].(<span style="color:#66d9ef">string</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">p</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ParsedJsonBody</span>) <span style="color:#a6e22e">GetInt</span>(<span style="color:#a6e22e">key</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">int</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// We don&#39;t need to check the type of the value, because trying</span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// to assert a non-int value to an int type will return the</span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// zero value for the int type (0 in this case)</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">data</span>[<span style="color:#a6e22e">key</span>].(<span style="color:#66d9ef">int</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></blockquote>
<p>It got more compact for sure, but the code is wrong. It got rid of the type checks with the confident explanation that type-asserting <em>a non-int value to an int type will return the zero value for the int type</em>, but this is not correct and will panic. Here&rsquo;s the code in Golang playground to try out: <a href="https://play.golang.com/p/8jjBJyrJ1uC">https://play.golang.com/p/8jjBJyrJ1uC</a></p>
<p>While impressive, at this point it&rsquo;s internal state of my program and what I&rsquo;m trying to do is a bit too messed up, and the confidently wrong explanation+implementation can be dangerous. For a moment I self-doubted my own knowledge of Golang and had to try this out in the playground to make sure.</p>
<p>The network isn&rsquo;t specifically trained on code, so I guess it just doesn&rsquo;t fully understand the inner-workings of Golang, without providing enough context to it first.</p>
<h2 id="generating-unit-tests">Generating unit tests</h2>
<p>Let&rsquo;s do something fun - unit tests (<em>groans</em>)! If it can spit out functional unit tests for stuff I&rsquo;m too lazy to write, that would already be very helpful. After all, it seems to understand code and is able to give explanations for what&rsquo;s going on.</p>
<p>The code here is a snippet from a JWT token parser. It validates the token, extracts sid/uid scopes (UUID) and checks if those are actually valid UUIDs.</p>
<blockquote>
<p>Me: Consider the following Golang code:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// other code here</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) <span style="color:#a6e22e">validateToken</span>(<span style="color:#a6e22e">t</span> <span style="color:#a6e22e">jwt</span>.<span style="color:#a6e22e">Token</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">Token</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">jwt</span>.<span style="color:#a6e22e">Validate</span>(<span style="color:#a6e22e">t</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// parse sid</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">sid</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;sid&#34;</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">ErrTokenInvalidSID</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">sidUUID</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">uuid</span>.<span style="color:#a6e22e">Parse</span>(<span style="color:#a6e22e">sid</span>.(<span style="color:#66d9ef">string</span>))
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">ErrTokenInvalidSID</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// parse uid</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">uid</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;uid&#34;</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">ErrTokenInvalidUID</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">uidUUID</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">uuid</span>.<span style="color:#a6e22e">Parse</span>(<span style="color:#a6e22e">uid</span>.(<span style="color:#66d9ef">string</span>))
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">ErrTokenInvalidUID</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">Token</span>{
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">SessionID</span>: <span style="color:#a6e22e">sidUUID</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">UserID</span>:    <span style="color:#a6e22e">uidUUID</span>,
</span></span><span style="display:flex;"><span>	}, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#75715e">// other code here</span>
</span></span></code></pre></div><p>Please generate some testcases for when sid or uid are invalid</p>
<p>ChatGPT: Here are some test cases for when the <code>sid</code> or <code>uid</code> values in the token are invalid:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestValidateTokenInvalidSID</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Create a new client</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewClient</span>(<span style="color:#e6db74">&#34;some-secret-key&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Create a test token</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">token</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">jwt</span>.<span style="color:#a6e22e">New</span>()
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">token</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#e6db74">&#34;uid&#34;</span>, <span style="color:#a6e22e">uuid</span>.<span style="color:#a6e22e">New</span>().<span style="color:#a6e22e">String</span>())
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Set the sid value to something invalid</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">token</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#e6db74">&#34;sid&#34;</span>, <span style="color:#e6db74">&#34;invalid-sid&#34;</span>)
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">token</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#a6e22e">jwt</span>.<span style="color:#a6e22e">ExpirationKey</span>, <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>().<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Minute</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">5</span>).<span style="color:#a6e22e">Unix</span>())
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Sign the token</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">signedToken</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">jwt</span>.<span style="color:#a6e22e">Sign</span>(<span style="color:#a6e22e">token</span>, <span style="color:#a6e22e">jwt</span>.<span style="color:#a6e22e">WithKey</span>(<span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">alg</span>, <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">hmacSecret</span>))
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>     <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;unexpected error signing token: %s&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Validate the token</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">validateToken</span>(<span style="color:#a6e22e">signedToken</span>)
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">ErrTokenInvalidSID</span> {
</span></span><span style="display:flex;"><span>     <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;expected error to be %s, got %s&#34;</span>, <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">ErrTokenInvalidSID</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestValidateTokenInvalidUID</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Create a new client</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewClient</span>(<span style="color:#e6db74">&#34;some-secret-key&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Create a test token</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">token</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">jwt</span>.<span style="color:#a6e22e">New</span>()
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Set the uid value to something invalid</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">token</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#e6db74">&#34;uid&#34;</span>, <span style="color:#e6db74">&#34;invalid-uid&#34;</span>)
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">token</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#e6db74">&#34;sid&#34;</span>, <span style="color:#a6e22e">uuid</span>.<span style="color:#a6e22e">New</span>().<span style="color:#a6e22e">String</span>())
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">token</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#a6e22e">jwt</span>.<span style="color:#a6e22e">ExpirationKey</span>, <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>().<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Minute</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">5</span>).<span style="color:#a6e22e">Unix</span>())
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Sign the token</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">signedToken</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">jwt</span>.<span style="color:#a6e22e">Sign</span>(<span style="color:#a6e22e">token</span>, <span style="color:#a6e22e">jwt</span>.<span style="color:#a6e22e">WithKey</span>(<span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">alg</span>, <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">hmacSecret</span>))
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>     <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;unexpected error signing token: %s&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Validate the token</span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">validateToken</span>(<span style="color:#a6e22e">signedToken</span>)
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">ErrTokenInvalidUID</span> {
</span></span><span style="display:flex;"><span>     <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;expected error to be %s, got %s&#34;</span>, <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">ErrTokenInvalidUID</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></blockquote>
<p>Sweet as. Sadly it&rsquo;s missing a bit of context here too, so I can&rsquo;t copy-paste the test as is. <code>jwt.Sign()</code> isn&rsquo;t returning a <code>jwt.Token</code>, but the jwt token in signed form, so it can&rsquo;t get passed to <code>validateToken</code>, but there&rsquo;s no way that it could have known that, without feeding the the jwt package into it first. Besides that it looks good, and the tests run after fixing that!</p>
<p>And to go full circle, now that we have more unit tests, let&rsquo;s ask it to optimize my <code>validateToken</code> function as well:</p>
<blockquote>
<p>One possible optimization for the <code>validateToken</code> function would be to move the code that parses the <code>sid</code> and <code>uid</code> values into a separate function, and then call that function from <code>validateToken</code> instead of duplicating the code. This would make the code easier to read and maintain, and would also avoid repeating the same error-checking code multiple times.</p>
<p>Here is an example of how the optimized <code>validateToken</code> function could look like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) <span style="color:#a6e22e">validateToken</span>(<span style="color:#a6e22e">t</span> <span style="color:#a6e22e">jwt</span>.<span style="color:#a6e22e">Token</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">Token</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Validate the token</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">jwt</span>.<span style="color:#a6e22e">Validate</span>(<span style="color:#a6e22e">t</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Parse the sid and uid values from the token</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">sid</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">parseSID</span>(<span style="color:#a6e22e">t</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">uid</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">parseUID</span>(<span style="color:#a6e22e">t</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">Token</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">SessionID</span>: <span style="color:#a6e22e">sid</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">UserID</span>:    <span style="color:#a6e22e">uid</span>,
</span></span><span style="display:flex;"><span>    }, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) <span style="color:#a6e22e">parseSID</span>(<span style="color:#a6e22e">t</span> <span style="color:#a6e22e">jwt</span>.<span style="color:#a6e22e">Token</span>) (<span style="color:#a6e22e">uuid</span>.<span style="color:#a6e22e">UUID</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// parse sid</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">sid</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;sid&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">uuid</span>.<span style="color:#a6e22e">UUID</span>{}, <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">ErrTokenInvalidSID</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">sidUUID</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">uuid</span>.<span style="color:#a6e22e">Parse</span>(<span style="color:#a6e22e">sid</span>.(<span style="color:#66d9ef">string</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">uuid</span>.<span style="color:#a6e22e">UUID</span>{}, <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">ErrTokenInvalidSID</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">sidUUID</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Client</span>) <span style="color:#a6e22e">parseUID</span>(<span style="color:#a6e22e">t</span> <span style="color:#a6e22e">jwt</span>.<span style="color:#a6e22e">Token</span>) (<span style="color:#a6e22e">uuid</span>.<span style="color:#a6e22e">UUID</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// parse uid</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">uid</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;uid&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">uuid</span>.<span style="color:#a6e22e">UUID</span>{}, <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">ErrTokenInvalidUID</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">uidUUID</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">uuid</span>.<span style="color:#a6e22e">Parse</span>(<span style="color:#a6e22e">uid</span>.(<span style="color:#66d9ef">string</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">uuid</span>.<span style="color:#a6e22e">UUID</span>{}, <span style="color:#a6e22e">domain</span>.<span style="color:#a6e22e">ErrTokenInvalidUID</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">uidUUID</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></blockquote>
<p>I was hoping it&rsquo;d catch the naked <code>.(string)</code> type assertion, but code splitting is also nice :D</p>
<p>Although this iteration doesn&rsquo;t really <em>avoid repeating the same error-checking code multiple times</em> like it claims.</p>
<h2 id="collaborative-hacking-with-an-ai-buddy">Collaborative hacking with an AI buddy</h2>
<p>I use Copilot quite a lot so I&rsquo;m already used to having an AI pair-programming buddy, but I strongly believe that as these tools get smarter, they will drastically change the way how we as engineers work.</p>
<p>While not there yet, AI will be able to generate more efficient code <em>faster than us</em>, spot errors we&rsquo;d miss, or flat-out generate entire applications based on spec input.</p>
<p>When Stable Diffusion came out, some people integrated it into their photo editors to quickly generate content based on a prompt. I am sure we&rsquo;ll see something like this, but for code, in no time. Imagine reading through some code, hitting an &ldquo;AI&rdquo; button, then ask it to refactor a routine, or give an explanation what that code does. The plugin will then send all the necessary context to the neural net and ask it to do the task.</p>
<p>/EDIT: someone already did it: <a href="https://twitter.com/marcelpociot/status/1599180144551526400">https://twitter.com/marcelpociot/status/1599180144551526400</a></p>
<p>The next generation of AI powered plugins could also do things like quickly provide feedback when it strongly believes a mistake has been made, or when it has a high confidence score for a completion / refactoring.</p>
<p>I think that would be really dang cool!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Release - Syncing app configurations with unisync</title>
      <link>https://david.coffee/unisync-release/</link>
      <pubDate>Sat, 19 Nov 2022 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/unisync-release/</guid><media:content url="https://david.coffee/unisync-release/unisync.jpg" medium="image" />
          <enclosure url="https://david.coffee/unisync-release/unisync.jpg" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/unisync-release/unisync.jpg&#34; /&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt;: &lt;a href=&#34;https://github.com/dvcrn/unisync&#34;&gt;unisync&lt;/a&gt; is a little tool I wrote that diffs and syncs your application settings across Macs, powered by unison&lt;/p&gt;
&lt;p&gt;Config management across machines is kind of a PITA. Some apps luckily natively support syncing settings, but most don&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;There are some solutions like the excellent &lt;a href=&#34;https://github.com/lra/mackup&#34;&gt;Mackup&lt;/a&gt; that try to fix this issue by moving all config files into a shared folder (like Dropbox), then creating symlinks from the application settings folders (eg &lt;code&gt;~/Library/Preferences/xxxx.plist&lt;/code&gt;) into that Dropbox folder.&lt;/p&gt;
&lt;p&gt;I used mackup for a while (even with my own &lt;a href=&#34;https://github.com/dvcrn/dotfiles/tree/master/mackup&#34;&gt;config files&lt;/a&gt;), but eventually got frustrated when symlinks randomly broke, for example, because the app decided it wanted to write the full file fresh to disk, effectively overwriting the symlink. Then some other apps detect symlinks and refuse to read them.&lt;/p&gt;
&lt;p&gt;I tried to mitigate that by &lt;a href=&#34;https://github.com/lra/mackup/pull/1779&#34;&gt;creating a PR to mackup&lt;/a&gt; that adds support for hardlinks, but tl;dr: hardlinks in APFS are not really hardlinks, and this is even more fragile with cloud storage than using symlinks.&lt;/p&gt;
&lt;h2 id=&#34;unison-is-great&#34;&gt;Unison is great&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/bcpierce00/unison&#34;&gt;unison&lt;/a&gt; is one of these really old tools that&amp;rsquo;s been around and developed forever. It&amp;rsquo;s a&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;*&amp;ldquo;Unison is a file-synchronization tool for POSIX-compliant systems (e.g. *BSD and GNU/Linux), macOS and Windows, with the caveat that the platform must be supported by OCaml&amp;rdquo;*&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;basically, it syncs files, kind of like a bi-directional rsync: Where rsync syncs one way (and alternatively deletes files), unison goes both ways and finds files that changed, got added or deleted across multiple directory nodes.&lt;/p&gt;
&lt;p&gt;Bi-directional sync makes it great for those pesky config dirs that could change on one host without us noticing. By diffing the directories against, say, a directory in Dropbox, unison can determine what actually changes, and copies the files accordingly.&lt;/p&gt;
&lt;p&gt;If a file is updated on Host B and is then synced through Dropbox to Host A, unison on Host A would see that the file in Dropbox is now newer than on the local one, and overwrite it with the version from Dropbox.&lt;/p&gt;
&lt;p&gt;unison can even sync &lt;em&gt;across&lt;/em&gt; machines directly!&lt;/p&gt;
&lt;h2 id=&#34;mackup--unison--unisync&#34;&gt;Mackup + Unison = Unisync&lt;/h2&gt;
&lt;p&gt;That brings us to &lt;a href=&#34;https://github.com/dvcrn/unisync&#34;&gt;unisync&lt;/a&gt;. Unisync is using the Mackup idea of having a library of app configurations that state where config files are, but instead of using symlinks, it calls into &lt;code&gt;unison&lt;/code&gt; to do the actual copying of files. So in theory, if files changed on one machine and then get synced to other machines, those files would get marked as changed / new, and get copied over the existing local file.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./unisync-screenshot.png&#34; alt=&#34;Unisync screenshot&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Because unisync is using unison under the hood, we can do more fancy things like providing a tree with files we want to handle, but add regex or naming patterns to ignore rules:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Raycast&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;friendlyName&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;raycast&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;files&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#f92672&#34;&gt;basePath&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;~/Library/Preferences/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;includedFiles&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#ae81ff&#34;&gt;com.raycast.macos.plist&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;ignoredFiles&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#ae81ff&#34;&gt;Name somethingToIgnore.plist&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Unisync also supports the &lt;a href=&#34;https://github.com/dvcrn/unisync/tree/main/apps/mackup&#34;&gt;entire Mackup app library&lt;/a&gt; for syncing to get you started, and comes with LaunchAgents that make it run automatically in the background.&lt;/p&gt;
&lt;p&gt;Check it out on GitHub and let me know if this tool is useful for you: &lt;a href=&#34;https://github.com/dvcrn/unisync&#34;&gt;https://github.com/dvcrn/unisync&lt;/a&gt;&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/unisync-release/unisync.jpg" /></p><p><strong>tl;dr</strong>: <a href="https://github.com/dvcrn/unisync">unisync</a> is a little tool I wrote that diffs and syncs your application settings across Macs, powered by unison</p>
<p>Config management across machines is kind of a PITA. Some apps luckily natively support syncing settings, but most don&rsquo;t.</p>
<p>There are some solutions like the excellent <a href="https://github.com/lra/mackup">Mackup</a> that try to fix this issue by moving all config files into a shared folder (like Dropbox), then creating symlinks from the application settings folders (eg <code>~/Library/Preferences/xxxx.plist</code>) into that Dropbox folder.</p>
<p>I used mackup for a while (even with my own <a href="https://github.com/dvcrn/dotfiles/tree/master/mackup">config files</a>), but eventually got frustrated when symlinks randomly broke, for example, because the app decided it wanted to write the full file fresh to disk, effectively overwriting the symlink. Then some other apps detect symlinks and refuse to read them.</p>
<p>I tried to mitigate that by <a href="https://github.com/lra/mackup/pull/1779">creating a PR to mackup</a> that adds support for hardlinks, but tl;dr: hardlinks in APFS are not really hardlinks, and this is even more fragile with cloud storage than using symlinks.</p>
<h2 id="unison-is-great">Unison is great</h2>
<p><a href="https://github.com/bcpierce00/unison">unison</a> is one of these really old tools that&rsquo;s been around and developed forever. It&rsquo;s a</p>
<blockquote>
<p>*&ldquo;Unison is a file-synchronization tool for POSIX-compliant systems (e.g. *BSD and GNU/Linux), macOS and Windows, with the caveat that the platform must be supported by OCaml&rdquo;*</p></blockquote>
<p>basically, it syncs files, kind of like a bi-directional rsync: Where rsync syncs one way (and alternatively deletes files), unison goes both ways and finds files that changed, got added or deleted across multiple directory nodes.</p>
<p>Bi-directional sync makes it great for those pesky config dirs that could change on one host without us noticing. By diffing the directories against, say, a directory in Dropbox, unison can determine what actually changes, and copies the files accordingly.</p>
<p>If a file is updated on Host B and is then synced through Dropbox to Host A, unison on Host A would see that the file in Dropbox is now newer than on the local one, and overwrite it with the version from Dropbox.</p>
<p>unison can even sync <em>across</em> machines directly!</p>
<h2 id="mackup--unison--unisync">Mackup + Unison = Unisync</h2>
<p>That brings us to <a href="https://github.com/dvcrn/unisync">unisync</a>. Unisync is using the Mackup idea of having a library of app configurations that state where config files are, but instead of using symlinks, it calls into <code>unison</code> to do the actual copying of files. So in theory, if files changed on one machine and then get synced to other machines, those files would get marked as changed / new, and get copied over the existing local file.</p>
<p>
<img class="img-fluid" src="./unisync-screenshot.png" alt="Unisync screenshot" />

</p>
<p>Because unisync is using unison under the hood, we can do more fancy things like providing a tree with files we want to handle, but add regex or naming patterns to ignore rules:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">Raycast</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">friendlyName</span>: <span style="color:#ae81ff">raycast</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">files</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">basePath</span>: <span style="color:#ae81ff">~/Library/Preferences/</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">includedFiles</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">com.raycast.macos.plist</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ignoredFiles</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">Name somethingToIgnore.plist</span>
</span></span></code></pre></div><p>Unisync also supports the <a href="https://github.com/dvcrn/unisync/tree/main/apps/mackup">entire Mackup app library</a> for syncing to get you started, and comes with LaunchAgents that make it run automatically in the background.</p>
<p>Check it out on GitHub and let me know if this tool is useful for you: <a href="https://github.com/dvcrn/unisync">https://github.com/dvcrn/unisync</a></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>On Crypto Burnout</title>
      <link>https://david.coffee/on-crypto-burnout/</link>
      <pubDate>Sun, 13 Nov 2022 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/on-crypto-burnout/</guid><media:content url="https://david.coffee/on-crypto-burnout/coins-falling.png" medium="image" />
          <enclosure url="https://david.coffee/on-crypto-burnout/coins-falling.png" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/on-crypto-burnout/coins-falling.png&#34; /&gt;&lt;/p&gt;&lt;p&gt;What a week, right? FTX, one of the biggest exchanges in the world, went from a reputable player that innovates the crypto space, to now rumors floating around that its ex-CEO is on the run, joining folks like Do Kwon and the guy from 3AC.&lt;/p&gt;
&lt;p&gt;What happened made me incredibly sad. I am a big proponent of cryptocurrency (after all, I built a wallet for Solana with Saifu) and am pretty active in the space. FTX was my favorite exchange, and I am very grateful that I was able to move 95% of my assets from FTX, roughly a day before withdrawals got halted (with the rest not movable due to open positions that were in the negative and didn’t want to close them off yet), but I still lost a good amount of money that’s now in eternal limbo, never to be seen again.&lt;/p&gt;
&lt;p&gt;Now there is more news - BlockFi halts withdrawals as well. Do you remember the time when ‘halting withdrawals’ was still unheard of? How the times have changed.&lt;/p&gt;
&lt;p&gt;Even though I got off fine this time around (I did lose a sizable sum on the UST collapse that was in Binance’s UST Earn product), I couldn’t help but notice a feeling of uneasiness. What if I hadn’t read Reddit or Crypto news on that particular day? 1/3 of my funds were on FTX, and that would have been gone now. In just 2-3 days! Crazy.&lt;/p&gt;
&lt;p&gt;But this also means that I can’t let my guard down. Instead of reading about crypto because I’m interested in it, I am now reading to make sure I am avoiding the next bomb.&lt;/p&gt;
&lt;h2 id=&#34;were-not-doing-so-great-right-now&#34;&gt;We’re not doing so great right now&lt;/h2&gt;
&lt;p&gt;With FTX and CEXes collapsing, one thing I constantly read is “not your keys not your crypto”, and “just put it into a ledger and don’t touch it”, both of which are absolutely true. But as a fan of crypto, I want to actually &lt;em&gt;do stuff&lt;/em&gt; with it, you know? What’s the point of a hyper-modern globally distributed digital currency network, if it has the same utility as a collection of rocks that you have in your closet, that may appreciate in value in the future?
Let me lend it out to get a bit of interest, stake some here and there, interact with protocols, borrow quickly against it when I go on vacation and need a bit of extra cash - you get the idea. But honestly, I don’t feel comfortable doing much of that currently.&lt;/p&gt;
&lt;p&gt;The DeFi world is a mess. Weekly rug pulls left and right, Smart Contracts get hacked, flash loan attacks, some guy finds an exploit to drain millions (and brags about it on Twitter), and poof - everyone&amp;rsquo;s money is gone. There are more reputable players like Aave that have been okay, but with even behemoths like Wormhole getting drained of funds, I’m not confident that it’ll stay this way.
Venus, for example, an Aave clone on BSC, had rumors of price manipulation to forcefully liquidate users. The temptation to make big money is just too big for some.&lt;/p&gt;
&lt;p&gt;The CeFi world is a mess (obviously). Don’t think I need to explain more considering recent events but - players like Blockfi/Celsius/Hodlnaut imploded out of nowhere, and FTX went up in flames. The money I had on Binance Earn (in UST) evaporated overnight (not Binances fault, I know). All while CEOs go publicly on Twitter to guarantee that yes, everything is indeed ok and there is no need to worry, only to a couple of days later disable withdrawals and turn the company with all user assets into a coffin.&lt;/p&gt;
&lt;p&gt;I practice risk management and am very careful with what I do with my money: I don’t have my stuff in one place, and I avoid &lt;em&gt;almost all&lt;/em&gt; DeFi protocols besides Aave. I only stake my stuff through native staking that’s at the core of the chain&amp;rsquo;s consensus mechanism, and never through third parties (like Lido). I evaluate CEXes very carefully and read everything I can find on how assets are handled and protected.&lt;/p&gt;
&lt;p&gt;I am the opposite of a degen gambler: A very informed tech person working in crypto, and &lt;em&gt;still&lt;/em&gt;, I am losing money.&lt;/p&gt;
&lt;p&gt;Besides having my crypto in a ledger and looking at it, I don’t feel much confidence in using it, and that’s bad. We absolutely should aim for having a vibrant ecosystem of DeFi and CeFi players, but the trust right now has been destroyed. People are scared of the next domino falling and someone running away with their hard-earned cash.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Have we hit crypto burnout?&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;mistrust-can-be-a-good-thing&#34;&gt;Mistrust can be a good thing&lt;/h2&gt;
&lt;p&gt;I wanted to leave it at that because the last sentence was a nice ending point, but had some more thoughts to share.&lt;/p&gt;
&lt;p&gt;Not all is bad of course. The current events caused a big wave of mistrust and that can be a good thing. Exchanges &lt;strong&gt;have to&lt;/strong&gt; be transparent about how funds are handled to earn their trust, and I can imagine seeing big boys like Celsius/FTX implode causes a lot of the remaining ones to sweat and rethink their investment strategies. Users become more careful about where they put their cash, and regulations are getting tighter. Maybe we&amp;rsquo;ll even see FDIC-like insurances popping up?&lt;/p&gt;
&lt;p&gt;What’s left after all of this is (hopefully) a more refined crypto space, with more transparency, more safety, and higher standards to hold CeFi/DeFi players to.&lt;/p&gt;
&lt;p&gt;We may have been set back by a couple of years, but maybe those years will now be a bit better.&lt;/p&gt;
&lt;p&gt;As for me, I will crank up my “risk management” lever. I moved most of my stuff into hardware wallets, moved to on-chain staking where I was able to, and reduced the amount of crypto &lt;em&gt;stuff&lt;/em&gt; I was using. I don’t trust anything DeFi that’s not Aave (very small amounts in Venus) and will continue with that attitude.&lt;/p&gt;
&lt;p&gt;In the CeFi world, I am a fan of &lt;a href=&#34;https://nexo.io/ref/d01ceqjakr?src=ios-link&#34;&gt;Nexo (ref link)&lt;/a&gt; - they have been very transparent so far, had no exposure to any recent events, survived multiple bank runs, and are headquartered in Europe, not the Bahamas. Still, only time can tell if they end up becoming the next domino. &lt;a href=&#34;https://www.binance.com/en/activity/referral-entry/CPA?fromActivityPage=true&amp;amp;ref=CPA_004FKE2JCR&#34;&gt;Binance&lt;/a&gt; is good, &lt;a href=&#34;https://crypto.com/exch/nq84hckvf8&#34;&gt;Crypto.com (exchange)&lt;/a&gt; is good too and so is Kraken. I’ll continue using all 3 of them.&lt;/p&gt;
&lt;p&gt;We’ll get through this. Let’s keep on innovating and building!&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/on-crypto-burnout/coins-falling.png" /></p><p>What a week, right? FTX, one of the biggest exchanges in the world, went from a reputable player that innovates the crypto space, to now rumors floating around that its ex-CEO is on the run, joining folks like Do Kwon and the guy from 3AC.</p>
<p>What happened made me incredibly sad. I am a big proponent of cryptocurrency (after all, I built a wallet for Solana with Saifu) and am pretty active in the space. FTX was my favorite exchange, and I am very grateful that I was able to move 95% of my assets from FTX, roughly a day before withdrawals got halted (with the rest not movable due to open positions that were in the negative and didn’t want to close them off yet), but I still lost a good amount of money that’s now in eternal limbo, never to be seen again.</p>
<p>Now there is more news - BlockFi halts withdrawals as well. Do you remember the time when ‘halting withdrawals’ was still unheard of? How the times have changed.</p>
<p>Even though I got off fine this time around (I did lose a sizable sum on the UST collapse that was in Binance’s UST Earn product), I couldn’t help but notice a feeling of uneasiness. What if I hadn’t read Reddit or Crypto news on that particular day? 1/3 of my funds were on FTX, and that would have been gone now. In just 2-3 days! Crazy.</p>
<p>But this also means that I can’t let my guard down. Instead of reading about crypto because I’m interested in it, I am now reading to make sure I am avoiding the next bomb.</p>
<h2 id="were-not-doing-so-great-right-now">We’re not doing so great right now</h2>
<p>With FTX and CEXes collapsing, one thing I constantly read is “not your keys not your crypto”, and “just put it into a ledger and don’t touch it”, both of which are absolutely true. But as a fan of crypto, I want to actually <em>do stuff</em> with it, you know? What’s the point of a hyper-modern globally distributed digital currency network, if it has the same utility as a collection of rocks that you have in your closet, that may appreciate in value in the future?
Let me lend it out to get a bit of interest, stake some here and there, interact with protocols, borrow quickly against it when I go on vacation and need a bit of extra cash - you get the idea. But honestly, I don’t feel comfortable doing much of that currently.</p>
<p>The DeFi world is a mess. Weekly rug pulls left and right, Smart Contracts get hacked, flash loan attacks, some guy finds an exploit to drain millions (and brags about it on Twitter), and poof - everyone&rsquo;s money is gone. There are more reputable players like Aave that have been okay, but with even behemoths like Wormhole getting drained of funds, I’m not confident that it’ll stay this way.
Venus, for example, an Aave clone on BSC, had rumors of price manipulation to forcefully liquidate users. The temptation to make big money is just too big for some.</p>
<p>The CeFi world is a mess (obviously). Don’t think I need to explain more considering recent events but - players like Blockfi/Celsius/Hodlnaut imploded out of nowhere, and FTX went up in flames. The money I had on Binance Earn (in UST) evaporated overnight (not Binances fault, I know). All while CEOs go publicly on Twitter to guarantee that yes, everything is indeed ok and there is no need to worry, only to a couple of days later disable withdrawals and turn the company with all user assets into a coffin.</p>
<p>I practice risk management and am very careful with what I do with my money: I don’t have my stuff in one place, and I avoid <em>almost all</em> DeFi protocols besides Aave. I only stake my stuff through native staking that’s at the core of the chain&rsquo;s consensus mechanism, and never through third parties (like Lido). I evaluate CEXes very carefully and read everything I can find on how assets are handled and protected.</p>
<p>I am the opposite of a degen gambler: A very informed tech person working in crypto, and <em>still</em>, I am losing money.</p>
<p>Besides having my crypto in a ledger and looking at it, I don’t feel much confidence in using it, and that’s bad. We absolutely should aim for having a vibrant ecosystem of DeFi and CeFi players, but the trust right now has been destroyed. People are scared of the next domino falling and someone running away with their hard-earned cash.</p>
<p><strong>Have we hit crypto burnout?</strong></p>
<h2 id="mistrust-can-be-a-good-thing">Mistrust can be a good thing</h2>
<p>I wanted to leave it at that because the last sentence was a nice ending point, but had some more thoughts to share.</p>
<p>Not all is bad of course. The current events caused a big wave of mistrust and that can be a good thing. Exchanges <strong>have to</strong> be transparent about how funds are handled to earn their trust, and I can imagine seeing big boys like Celsius/FTX implode causes a lot of the remaining ones to sweat and rethink their investment strategies. Users become more careful about where they put their cash, and regulations are getting tighter. Maybe we&rsquo;ll even see FDIC-like insurances popping up?</p>
<p>What’s left after all of this is (hopefully) a more refined crypto space, with more transparency, more safety, and higher standards to hold CeFi/DeFi players to.</p>
<p>We may have been set back by a couple of years, but maybe those years will now be a bit better.</p>
<p>As for me, I will crank up my “risk management” lever. I moved most of my stuff into hardware wallets, moved to on-chain staking where I was able to, and reduced the amount of crypto <em>stuff</em> I was using. I don’t trust anything DeFi that’s not Aave (very small amounts in Venus) and will continue with that attitude.</p>
<p>In the CeFi world, I am a fan of <a href="https://nexo.io/ref/d01ceqjakr?src=ios-link">Nexo (ref link)</a> - they have been very transparent so far, had no exposure to any recent events, survived multiple bank runs, and are headquartered in Europe, not the Bahamas. Still, only time can tell if they end up becoming the next domino. <a href="https://www.binance.com/en/activity/referral-entry/CPA?fromActivityPage=true&amp;ref=CPA_004FKE2JCR">Binance</a> is good, <a href="https://crypto.com/exch/nq84hckvf8">Crypto.com (exchange)</a> is good too and so is Kraken. I’ll continue using all 3 of them.</p>
<p>We’ll get through this. Let’s keep on innovating and building!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Why and how I use k8s for my personal stuff (and love it)</title>
      <link>https://david.coffee/why-and-how-i-use-k8s-for-personal-stuff/</link>
      <pubDate>Sat, 05 Nov 2022 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/why-and-how-i-use-k8s-for-personal-stuff/</guid><media:content url="https://david.coffee/why-and-how-i-use-k8s-for-personal-stuff/whale-through-space.png" medium="image" />
          <enclosure url="https://david.coffee/why-and-how-i-use-k8s-for-personal-stuff/whale-through-space.png" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/why-and-how-i-use-k8s-for-personal-stuff/whale-through-space.png&#34; /&gt;&lt;/p&gt;&lt;p&gt;I&amp;rsquo;m one of those odd nuts that love kubernetes. I like it so much that it&amp;rsquo;s currently powering a lot of my personal stuff and hobby projects. So instead of the usual &amp;ldquo;k8s is bloated&amp;rdquo;, &amp;ldquo;k8s is overkill&amp;rdquo; or &amp;ldquo;why you don&amp;rsquo;t need k8s&amp;rdquo; posts, today let&amp;rsquo;s talk about why k8s is actually great for personal stuff, and why you should maybe also consider using it? :)&lt;/p&gt;
&lt;h2 id=&#34;1-why-i-love-kubernetes-for-personal-stuff&#34;&gt;1. Why I love kubernetes for personal stuff&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s start with the &lt;em&gt;why&lt;/em&gt;. Why would I use something as bloated and heavy as kubernetes for personal stuff, isn&amp;rsquo;t it a huge overkill? I could go on and on about why it&amp;rsquo;s great, but let&amp;rsquo;s take a look at the big reasons.&lt;/p&gt;
&lt;p&gt;I want to also preface that small managed clusters can be really cheap. My main managed &amp;ldquo;cluster&amp;rdquo; (putting it in quotes because it only has 1 node) on &lt;a href=&#34;https://www.digitalocean.com/products/kubernetes&#34;&gt;digitalocean&lt;/a&gt; costs $12/month. Compare that with other hosting providers.&lt;/p&gt;
&lt;h3 id=&#34;infra-as-code-&#34;&gt;Infra as code 👨‍💻&lt;/h3&gt;
&lt;p&gt;k8s, same as with &lt;a href=&#34;https://www.terraform.io&#34;&gt;terraform&lt;/a&gt;, allows me to specify my infrastructure as code. I write &lt;em&gt;manifest&lt;/em&gt; files that specify how stuff is going to get run. If I want to make a change to my infra, all I need to do is update those manifests, apply them, and I&amp;rsquo;m done. Look at this snippet from a manifest that tells k8s that I want to run a webdav container:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;containers&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;webdav&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;bytemark/webdav&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;ports&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            - &lt;span style=&#34;color:#f92672&#34;&gt;containerPort&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;80&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;env&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;USERNAME&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            	&lt;span style=&#34;color:#f92672&#34;&gt;value&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;myuser&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;PASSWORD&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#f92672&#34;&gt;valueFrom&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#f92672&#34;&gt;secretKeyRef&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;webdav-credentials&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  &lt;span style=&#34;color:#f92672&#34;&gt;key&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;password&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Because I have everything in code, I can set up my entire cluster from scratch on any provider I want. If I no longer like digitalocean, all I have to do is create a cluster somewhere else and do a &lt;code&gt;kubectl apply&lt;/code&gt;. The new setup will be identical (or almost identical) to my previous one, it&amp;rsquo;s almost provider agnostic (there are some provider-specific things like persistent volumes, but we&amp;rsquo;ll get to those later).&lt;/p&gt;
&lt;p&gt;Manifests are also idempotent, I can re-apply the same files a dozen times without worry of breaking anything. If resources already exist, they stay existent and if there are no changes, the manifest will just get ignored.&lt;/p&gt;
&lt;h3 id=&#34;extremely-easy-to-add-storage-or-ip-addresses-&#34;&gt;Extremely easy to add storage or IP addresses 🔄&lt;/h3&gt;
&lt;p&gt;Managed kubernetes clusters (like the digitalocean one) are integrated with the entire cloud platform of that provider, so it&amp;rsquo;s &lt;em&gt;very&lt;/em&gt; easy to do things like adding block storage or loadbalancers. How easy? Check this out:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;apiVersion&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;kind&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;PersistentVolumeClaim&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;some-name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;accessModes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;ReadWriteOnce&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;resources&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;requests&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;storage&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;1Gi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;storageClassName&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;do-block-storage&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This manifest when applied will tell DigitalOcean (through the kubernetes controller) to provision a 1Gb block storage volume. If that 1Gb fills up at some point, I change it to 10Gb, re-apply and DigitalOcean will automatically resize the volume to 10GB for me - neat!&lt;/p&gt;
&lt;p&gt;Even cooler, I can now use this storage volume on any of my containers that are running inside my cluster. All I need to do is add a mount point into the manifest, and suddenly ephemeral containers have a way to write persistent data:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;some-name-volume&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;persistentVolumeClaim&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;claimName&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;some-name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;readOnly&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If I want to get rid of the volume, I do &lt;code&gt;kubectl delete pvc some-name&lt;/code&gt; and it gets wiped. (There is a &lt;em&gt;retain&lt;/em&gt; setting if you don&amp;rsquo;t want volume content to get deleted)&lt;/p&gt;
&lt;p&gt;This isn&amp;rsquo;t just storage though, the same thing happens with stuff like &lt;em&gt;LoadBalancers&lt;/em&gt; and egress IPs. Provisioning resources on kubernetes causes DigitalOcean to get a static IP + LoadBalancer ready for us, wait until it&amp;rsquo;s good to go, then assign it to the cluster.&lt;/p&gt;
&lt;h3 id=&#34;it-makes-running-stuff-and-keeping-stuff-running-a-breeze-&#34;&gt;It makes running stuff and keeping stuff running a breeze 🍃&lt;/h3&gt;
&lt;p&gt;K8s is very battle-tested. A lot of companies run on kubernetes so it&amp;rsquo;s grown to be robust and fault-tolerant. And if something does end up not working or acting up, I can be sure that I will find the answer to my issue in no time.&lt;/p&gt;
&lt;p&gt;Besides running containers, k8s does things like keeping those running (if you want that). Let&amp;rsquo;s take a look at the webdav example from above, but conjure up some more yaml&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;apiVersion&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;apps/v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;kind&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Deployment&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;webdav&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;replicas&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;selector&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;matchLabels&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;app&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;webdav&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;template&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;labels&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;app&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;webdav&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;containers&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;webdav&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;bytemark/webdav&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;ports&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            - &lt;span style=&#34;color:#f92672&#34;&gt;containerPort&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;80&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;env&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;USERNAME&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            	&lt;span style=&#34;color:#f92672&#34;&gt;value&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;myuser&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;PASSWORD&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#f92672&#34;&gt;valueFrom&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#f92672&#34;&gt;secretKeyRef&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;webdav-credentials&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  &lt;span style=&#34;color:#f92672&#34;&gt;key&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;password&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The webdav container is now in a &lt;em&gt;Deployment&lt;/em&gt; with 1 replica. This means k8s will make sure there is &lt;em&gt;always&lt;/em&gt; 1 webdav container running. You can even &lt;code&gt;kubectl delete pod &amp;lt;webdav-xxxx&amp;gt;&lt;/code&gt; to destroy the webdav container, and kubernetes will just respawn a new one.&lt;/p&gt;
&lt;p&gt;My server can crash, the container can crash - doesn&amp;rsquo;t matter, don&amp;rsquo;t care. In the end, kubernetes will jump in and just scale a new container up again.&lt;/p&gt;
&lt;p&gt;And if I want more containers? All I have to do is change &lt;code&gt;replicas: 1&lt;/code&gt; to &lt;code&gt;replicas: 2&lt;/code&gt;, and now I will always have 2 webdav containers. This makes scaling things up and down as easy as changing a yaml file and applying it.&lt;/p&gt;
&lt;p&gt;But there are other cool things that &lt;em&gt;just work&lt;/em&gt; like CronJobs! I can specify my CronJob yaml manifests, tell kubernetes when it should run those, and I can be sure they are getting run. No messing with crontab or some cloud scheduling thing, I can do it right from within k8s, together with the rest of my infra.&lt;/p&gt;
&lt;h3 id=&#34;easy-scaling-when-it-becomes-necessary-&#34;&gt;Easy scaling when it becomes necessary 📈&lt;/h3&gt;
&lt;p&gt;So we saw in the previous section that we can scale containers however we please. It gets even cooler if we deal with multiple nodes on the cluster. Say my hobby project is getting more popular - instead of a simple web server I had to add a database and maybe a queue server. Resources are getting tight, and stuff doesn&amp;rsquo;t run as nicely any more.&lt;/p&gt;
&lt;p&gt;What I can do is, I can open up the DigitalOcean kubernetes admin, go to my node settings, and either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Increase the nodes of the cluster to 2&lt;/li&gt;
&lt;li&gt;Destroy the current 1-node setup and replace it with a stronger node&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because k8s is managing the cluster, even if we destroy the node and add a new, stronger node in its place, it will still do exactly what it did before: Make sure those containers are running in the configuration we specified as soon as the new machine is up and running.&lt;/p&gt;
&lt;p&gt;If we increase the cluster size from 1 to 2, kubernetes will look at available resources on the cluster, see that a new node is available, and balance those containers for us, by maybe moving the queue server to the new machine, or having one of those 2 webdav replicas there instead. (Of course, if we want less magic we can also tell it specifically what it should do and how it should utilize those nodes, like always having one replica on each node)&lt;/p&gt;
&lt;p&gt;It goes the other way as well: We&amp;rsquo;re getting tight on money so we decided to cut down on resources. Removing a node from the cluster will make k8s reshuffle our containers and consolidate them onto the remaining nodes, and THEN shut the node down, without downtime.&lt;/p&gt;
&lt;h2 id=&#34;2-how-i-use-kubernetes-for-my-personal-stuff&#34;&gt;2. How I use kubernetes for my personal stuff&lt;/h2&gt;
&lt;p&gt;Now that you&amp;rsquo;ve heard the main reasons why I like kubernetes, let&amp;rsquo;s take a look at how I specifically am using it on a daily basis.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s start with the provider - I mentioned that I use DigitalOcean kubernetes for my cloud of choice. I like them because the cost is predictable and the managed cluster is cheap. I am always sure how my bill will look like and there are no surprises. At $12/node for kubernetes I really have no complaints here. It&amp;rsquo;s great! This price doesn&amp;rsquo;t include a LoadBalancer/static IP but for most stuff I don&amp;rsquo;t really need those.&lt;/p&gt;
&lt;p&gt;k8s can do a lot, but the parts that I actually use are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deployments: Fancy way of saying &amp;ldquo;keep this many replicas running at all time&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Services: Fancy way of saying &amp;ldquo;I need an cluster internal/external IP for these containers&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Ingress: A fancy router like &amp;ldquo;route domain david.coffee to this pod&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Cron&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;exposing-stuff-for-personal-use-only-with-tailscale&#34;&gt;Exposing stuff for personal use only with tailscale&lt;/h3&gt;
&lt;p&gt;Most of my stuff doesn&amp;rsquo;t need to be public. It&amp;rsquo;s either a cronjob that does something and doesn&amp;rsquo;t need inbound connections, or it&amp;rsquo;s stuff that&amp;rsquo;s only public to me.&lt;/p&gt;
&lt;p&gt;To make things public to myself only, I am using &lt;a href=&#34;https://tailscale.com&#34;&gt;tailscale&lt;/a&gt; as my overlay network of choice (other great options are ZeroTier and Slack Nebula). Tailscale utilizes wireguard tunnels to build a Layer 3 p2p network that most of the times &lt;em&gt;Just Works&lt;/em&gt;™️&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./mesh-network.svg&#34; alt=&#34;mesh-network&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;The cool thing is, I can embed the sidecar container into any kubernetes pod I want to access, and it becomes available to my tailscale network. All I have to do is add some yaml to a pod:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;containers&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;webdav&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;bytemark/webdav&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;ports&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            - &lt;span style=&#34;color:#f92672&#34;&gt;containerPort&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;80&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;env&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;USERNAME&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            	&lt;span style=&#34;color:#f92672&#34;&gt;value&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;myuser&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;PASSWORD&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#f92672&#34;&gt;valueFrom&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#f92672&#34;&gt;secretKeyRef&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;webdav-credentials&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  &lt;span style=&#34;color:#f92672&#34;&gt;key&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;password&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;ts-sidecar&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;imagePullPolicy&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Always&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ghcr.io/tailscale/tailscale:latest&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;env&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;TS_KUBE_SECRET&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#f92672&#34;&gt;value&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tailscale-webdav&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;TS_USERSPACE&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#f92672&#34;&gt;value&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;false&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;TS_AUTH_KEY&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#f92672&#34;&gt;valueFrom&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#f92672&#34;&gt;secretKeyRef&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;tailscale-auth&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  &lt;span style=&#34;color:#f92672&#34;&gt;key&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;TS_AUTH_KEY&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;securityContext&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;capabilities&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#f92672&#34;&gt;add&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                - &lt;span style=&#34;color:#ae81ff&#34;&gt;NET_ADMIN&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&amp;hellip; and when my connection is established, I can just do &lt;code&gt;http://webdav-tailscale:80&lt;/code&gt; and I have a fully personal connection to my webdav container. No ingress or load-balancer needed ✌️&lt;/p&gt;
&lt;p&gt;A non-tailscale option would be to use port-forwarding into the pod directly:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# Listen on port 8888 locally, forwarding to 80 in the pod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  kubectl port-forward pod/mypod 8888:80
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;exposing-public-stuff-without-static-ip--loadbalancer-with-nginx-the-frugal-option&#34;&gt;Exposing public stuff without static IP + LoadBalancer with Nginx (the frugal option)&lt;/h3&gt;
&lt;p&gt;Because my cluster has only 1 node, I almost never need a proper loadbalancer or static IP.&lt;/p&gt;
&lt;p&gt;My ingress controller of choice is &lt;a href=&#34;https://github.com/kubernetes/ingress-nginx&#34;&gt;nginx&lt;/a&gt;, and we can tweak it to run off a ClusterIP service instead of a full blown load balancer:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;helm install ingress-nginx ingress-nginx/ingress-nginx -f nginx-ingress.yml
&lt;/code&gt;&lt;/pre&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;controller&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;kind&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;DaemonSet&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;daemonset&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;useHostPort&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;dnsPolicy&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;ClusterFirstWithHostNet&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;hostNetwork&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;service&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;type&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;ClusterIP&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;resources&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;requests&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;cpu&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;10m&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;rbac&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;create&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A &lt;code&gt;DaemonSet&lt;/code&gt; is something we haven&amp;rsquo;t talked about yet, it&amp;rsquo;s similar to a Deployment with the exception that it makes sure the &lt;em&gt;thing&lt;/em&gt; is running on all nodes of the cluster. So if the cluster has 1 node, there&amp;rsquo;ll be one of those. If there are 3 nodes, we&amp;rsquo;ll have 3 nginx controllers.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hostNetwork: true&lt;/code&gt; is running that service off the host directly, so we can hook up &lt;code&gt;&amp;lt;machineip&amp;gt;:80&lt;/code&gt; and let nginx handle the routing.&lt;/p&gt;
&lt;p&gt;Then once we have that contorller running we can add actual Ingress resources to do something based on domain name like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;apiVersion&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;kind&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Ingress&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;ingress&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;ingressClassName&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;nginx&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;defaultBackend&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;service&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;ingress-nginx-controller&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;port&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;number&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;80&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;rules&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#f92672&#34;&gt;host&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;somethingsomething.david.coffee&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;http&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;paths&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          - &lt;span style=&#34;color:#f92672&#34;&gt;pathType&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Prefix&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;path&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;backend&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#f92672&#34;&gt;service&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;somethingsomething-service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#f92672&#34;&gt;port&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  &lt;span style=&#34;color:#f92672&#34;&gt;number&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;80&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To make sure the IP stays up to date, we can hook up a cronjob and update the cloudflare DNS records automatically, for maximum money saving.&lt;/p&gt;
&lt;p&gt;Of course if you have the cash and want to scale up, just get a proper static IP with &lt;code&gt;LoadBalancer&lt;/code&gt;. It&amp;rsquo;s only $10 extra.&lt;/p&gt;
&lt;h2 id=&#34;happy-whale-ing&#34;&gt;Happy whale-ing&lt;/h2&gt;
&lt;p&gt;As you can tell by this long post, I am a fan of kubernetes and use it for my personal stuff &lt;em&gt;a lot&lt;/em&gt;. If you read until this point, congratulations! You made it. Let me know &lt;a href=&#34;https://twitter.com/dvcrn&#34;&gt;on twitter&lt;/a&gt; if you think this kind of post is useful, or if there are parts you&amp;rsquo;d like to know more about.&lt;/p&gt;
&lt;p&gt;Happy whale-ing! 🐳&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./whale-space-stars.png&#34; alt=&#34;whale-space-stars&#34; /&gt;

&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/why-and-how-i-use-k8s-for-personal-stuff/whale-through-space.png" /></p><p>I&rsquo;m one of those odd nuts that love kubernetes. I like it so much that it&rsquo;s currently powering a lot of my personal stuff and hobby projects. So instead of the usual &ldquo;k8s is bloated&rdquo;, &ldquo;k8s is overkill&rdquo; or &ldquo;why you don&rsquo;t need k8s&rdquo; posts, today let&rsquo;s talk about why k8s is actually great for personal stuff, and why you should maybe also consider using it? :)</p>
<h2 id="1-why-i-love-kubernetes-for-personal-stuff">1. Why I love kubernetes for personal stuff</h2>
<p>Let&rsquo;s start with the <em>why</em>. Why would I use something as bloated and heavy as kubernetes for personal stuff, isn&rsquo;t it a huge overkill? I could go on and on about why it&rsquo;s great, but let&rsquo;s take a look at the big reasons.</p>
<p>I want to also preface that small managed clusters can be really cheap. My main managed &ldquo;cluster&rdquo; (putting it in quotes because it only has 1 node) on <a href="https://www.digitalocean.com/products/kubernetes">digitalocean</a> costs $12/month. Compare that with other hosting providers.</p>
<h3 id="infra-as-code-">Infra as code 👨‍💻</h3>
<p>k8s, same as with <a href="https://www.terraform.io">terraform</a>, allows me to specify my infrastructure as code. I write <em>manifest</em> files that specify how stuff is going to get run. If I want to make a change to my infra, all I need to do is update those manifests, apply them, and I&rsquo;m done. Look at this snippet from a manifest that tells k8s that I want to run a webdav container:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>      <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">webdav</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">bytemark/webdav</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">containerPort</span>: <span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;USERNAME&#34;</span>
</span></span><span style="display:flex;"><span>            	<span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;myuser&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;PASSWORD&#34;</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">webdav-credentials</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">password</span>
</span></span></code></pre></div><p>Because I have everything in code, I can set up my entire cluster from scratch on any provider I want. If I no longer like digitalocean, all I have to do is create a cluster somewhere else and do a <code>kubectl apply</code>. The new setup will be identical (or almost identical) to my previous one, it&rsquo;s almost provider agnostic (there are some provider-specific things like persistent volumes, but we&rsquo;ll get to those later).</p>
<p>Manifests are also idempotent, I can re-apply the same files a dozen times without worry of breaking anything. If resources already exist, they stay existent and if there are no changes, the manifest will just get ignored.</p>
<h3 id="extremely-easy-to-add-storage-or-ip-addresses-">Extremely easy to add storage or IP addresses 🔄</h3>
<p>Managed kubernetes clusters (like the digitalocean one) are integrated with the entire cloud platform of that provider, so it&rsquo;s <em>very</em> easy to do things like adding block storage or loadbalancers. How easy? Check this out:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">PersistentVolumeClaim</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">some-name</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">accessModes</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">ReadWriteOnce</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">storage</span>: <span style="color:#ae81ff">1Gi</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">storageClassName</span>: <span style="color:#ae81ff">do-block-storage</span>
</span></span></code></pre></div><p>This manifest when applied will tell DigitalOcean (through the kubernetes controller) to provision a 1Gb block storage volume. If that 1Gb fills up at some point, I change it to 10Gb, re-apply and DigitalOcean will automatically resize the volume to 10GB for me - neat!</p>
<p>Even cooler, I can now use this storage volume on any of my containers that are running inside my cluster. All I need to do is add a mount point into the manifest, and suddenly ephemeral containers have a way to write persistent data:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">some-name-volume</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">persistentVolumeClaim</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">claimName</span>: <span style="color:#ae81ff">some-name</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">readOnly</span>: <span style="color:#66d9ef">false</span>
</span></span></code></pre></div><p>If I want to get rid of the volume, I do <code>kubectl delete pvc some-name</code> and it gets wiped. (There is a <em>retain</em> setting if you don&rsquo;t want volume content to get deleted)</p>
<p>This isn&rsquo;t just storage though, the same thing happens with stuff like <em>LoadBalancers</em> and egress IPs. Provisioning resources on kubernetes causes DigitalOcean to get a static IP + LoadBalancer ready for us, wait until it&rsquo;s good to go, then assign it to the cluster.</p>
<h3 id="it-makes-running-stuff-and-keeping-stuff-running-a-breeze-">It makes running stuff and keeping stuff running a breeze 🍃</h3>
<p>K8s is very battle-tested. A lot of companies run on kubernetes so it&rsquo;s grown to be robust and fault-tolerant. And if something does end up not working or acting up, I can be sure that I will find the answer to my issue in no time.</p>
<p>Besides running containers, k8s does things like keeping those running (if you want that). Let&rsquo;s take a look at the webdav example from above, but conjure up some more yaml</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">apps/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Deployment</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">webdav</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">replicas</span>: <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">selector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app</span>: <span style="color:#ae81ff">webdav</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">template</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">app</span>: <span style="color:#ae81ff">webdav</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">webdav</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">bytemark/webdav</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">containerPort</span>: <span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;USERNAME&#34;</span>
</span></span><span style="display:flex;"><span>            	<span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;myuser&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;PASSWORD&#34;</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">webdav-credentials</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">password</span>
</span></span></code></pre></div><p>The webdav container is now in a <em>Deployment</em> with 1 replica. This means k8s will make sure there is <em>always</em> 1 webdav container running. You can even <code>kubectl delete pod &lt;webdav-xxxx&gt;</code> to destroy the webdav container, and kubernetes will just respawn a new one.</p>
<p>My server can crash, the container can crash - doesn&rsquo;t matter, don&rsquo;t care. In the end, kubernetes will jump in and just scale a new container up again.</p>
<p>And if I want more containers? All I have to do is change <code>replicas: 1</code> to <code>replicas: 2</code>, and now I will always have 2 webdav containers. This makes scaling things up and down as easy as changing a yaml file and applying it.</p>
<p>But there are other cool things that <em>just work</em> like CronJobs! I can specify my CronJob yaml manifests, tell kubernetes when it should run those, and I can be sure they are getting run. No messing with crontab or some cloud scheduling thing, I can do it right from within k8s, together with the rest of my infra.</p>
<h3 id="easy-scaling-when-it-becomes-necessary-">Easy scaling when it becomes necessary 📈</h3>
<p>So we saw in the previous section that we can scale containers however we please. It gets even cooler if we deal with multiple nodes on the cluster. Say my hobby project is getting more popular - instead of a simple web server I had to add a database and maybe a queue server. Resources are getting tight, and stuff doesn&rsquo;t run as nicely any more.</p>
<p>What I can do is, I can open up the DigitalOcean kubernetes admin, go to my node settings, and either:</p>
<ul>
<li>Increase the nodes of the cluster to 2</li>
<li>Destroy the current 1-node setup and replace it with a stronger node</li>
</ul>
<p>Because k8s is managing the cluster, even if we destroy the node and add a new, stronger node in its place, it will still do exactly what it did before: Make sure those containers are running in the configuration we specified as soon as the new machine is up and running.</p>
<p>If we increase the cluster size from 1 to 2, kubernetes will look at available resources on the cluster, see that a new node is available, and balance those containers for us, by maybe moving the queue server to the new machine, or having one of those 2 webdav replicas there instead. (Of course, if we want less magic we can also tell it specifically what it should do and how it should utilize those nodes, like always having one replica on each node)</p>
<p>It goes the other way as well: We&rsquo;re getting tight on money so we decided to cut down on resources. Removing a node from the cluster will make k8s reshuffle our containers and consolidate them onto the remaining nodes, and THEN shut the node down, without downtime.</p>
<h2 id="2-how-i-use-kubernetes-for-my-personal-stuff">2. How I use kubernetes for my personal stuff</h2>
<p>Now that you&rsquo;ve heard the main reasons why I like kubernetes, let&rsquo;s take a look at how I specifically am using it on a daily basis.</p>
<p>Let&rsquo;s start with the provider - I mentioned that I use DigitalOcean kubernetes for my cloud of choice. I like them because the cost is predictable and the managed cluster is cheap. I am always sure how my bill will look like and there are no surprises. At $12/node for kubernetes I really have no complaints here. It&rsquo;s great! This price doesn&rsquo;t include a LoadBalancer/static IP but for most stuff I don&rsquo;t really need those.</p>
<p>k8s can do a lot, but the parts that I actually use are:</p>
<ul>
<li>Deployments: Fancy way of saying &ldquo;keep this many replicas running at all time&rdquo;</li>
<li>Services: Fancy way of saying &ldquo;I need an cluster internal/external IP for these containers&rdquo;</li>
<li>Ingress: A fancy router like &ldquo;route domain david.coffee to this pod&rdquo;</li>
<li>Cron</li>
</ul>
<h3 id="exposing-stuff-for-personal-use-only-with-tailscale">Exposing stuff for personal use only with tailscale</h3>
<p>Most of my stuff doesn&rsquo;t need to be public. It&rsquo;s either a cronjob that does something and doesn&rsquo;t need inbound connections, or it&rsquo;s stuff that&rsquo;s only public to me.</p>
<p>To make things public to myself only, I am using <a href="https://tailscale.com">tailscale</a> as my overlay network of choice (other great options are ZeroTier and Slack Nebula). Tailscale utilizes wireguard tunnels to build a Layer 3 p2p network that most of the times <em>Just Works</em>™️</p>
<p>
<img class="img-fluid" src="./mesh-network.svg" alt="mesh-network" />

</p>
<p>The cool thing is, I can embed the sidecar container into any kubernetes pod I want to access, and it becomes available to my tailscale network. All I have to do is add some yaml to a pod:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>      <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">webdav</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">bytemark/webdav</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">containerPort</span>: <span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;USERNAME&#34;</span>
</span></span><span style="display:flex;"><span>            	<span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;myuser&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;PASSWORD&#34;</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">webdav-credentials</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">password</span>
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ts-sidecar</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">imagePullPolicy</span>: <span style="color:#ae81ff">Always</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#e6db74">&#34;ghcr.io/tailscale/tailscale:latest&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">TS_KUBE_SECRET</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;tailscale-webdav&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">TS_USERSPACE</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;false&#34;</span>
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">TS_AUTH_KEY</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">valueFrom</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">secretKeyRef</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">tailscale-auth</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">key</span>: <span style="color:#ae81ff">TS_AUTH_KEY</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">securityContext</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">capabilities</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">add</span>:
</span></span><span style="display:flex;"><span>                - <span style="color:#ae81ff">NET_ADMIN</span>
</span></span></code></pre></div><p>&hellip; and when my connection is established, I can just do <code>http://webdav-tailscale:80</code> and I have a fully personal connection to my webdav container. No ingress or load-balancer needed ✌️</p>
<p>A non-tailscale option would be to use port-forwarding into the pod directly:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>  <span style="color:#75715e"># Listen on port 8888 locally, forwarding to 80 in the pod</span>
</span></span><span style="display:flex;"><span>  kubectl port-forward pod/mypod 8888:80
</span></span></code></pre></div><h3 id="exposing-public-stuff-without-static-ip--loadbalancer-with-nginx-the-frugal-option">Exposing public stuff without static IP + LoadBalancer with Nginx (the frugal option)</h3>
<p>Because my cluster has only 1 node, I almost never need a proper loadbalancer or static IP.</p>
<p>My ingress controller of choice is <a href="https://github.com/kubernetes/ingress-nginx">nginx</a>, and we can tweak it to run off a ClusterIP service instead of a full blown load balancer:</p>
<pre tabindex="0"><code>helm install ingress-nginx ingress-nginx/ingress-nginx -f nginx-ingress.yml
</code></pre><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#f92672">controller</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">DaemonSet</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">daemonset</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">useHostPort</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">dnsPolicy</span>: <span style="color:#ae81ff">ClusterFirstWithHostNet</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hostNetwork</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">service</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">ClusterIP</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">cpu</span>: <span style="color:#ae81ff">10m</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">rbac</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">create</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>A <code>DaemonSet</code> is something we haven&rsquo;t talked about yet, it&rsquo;s similar to a Deployment with the exception that it makes sure the <em>thing</em> is running on all nodes of the cluster. So if the cluster has 1 node, there&rsquo;ll be one of those. If there are 3 nodes, we&rsquo;ll have 3 nginx controllers.</p>
<p><code>hostNetwork: true</code> is running that service off the host directly, so we can hook up <code>&lt;machineip&gt;:80</code> and let nginx handle the routing.</p>
<p>Then once we have that contorller running we can add actual Ingress resources to do something based on domain name like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">networking.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Ingress</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ingress</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ingressClassName</span>: <span style="color:#ae81ff">nginx</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">defaultBackend</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">service</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">name</span>: <span style="color:#ae81ff">ingress-nginx-controller</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">port</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">number</span>: <span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">host</span>: <span style="color:#ae81ff">somethingsomething.david.coffee</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">http</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">paths</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">pathType</span>: <span style="color:#ae81ff">Prefix</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">backend</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">service</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">name</span>: <span style="color:#ae81ff">somethingsomething-service</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">port</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">number</span>: <span style="color:#ae81ff">80</span>
</span></span></code></pre></div><p>To make sure the IP stays up to date, we can hook up a cronjob and update the cloudflare DNS records automatically, for maximum money saving.</p>
<p>Of course if you have the cash and want to scale up, just get a proper static IP with <code>LoadBalancer</code>. It&rsquo;s only $10 extra.</p>
<h2 id="happy-whale-ing">Happy whale-ing</h2>
<p>As you can tell by this long post, I am a fan of kubernetes and use it for my personal stuff <em>a lot</em>. If you read until this point, congratulations! You made it. Let me know <a href="https://twitter.com/dvcrn">on twitter</a> if you think this kind of post is useful, or if there are parts you&rsquo;d like to know more about.</p>
<p>Happy whale-ing! 🐳</p>
<p>
<img class="img-fluid" src="./whale-space-stars.png" alt="whale-space-stars" />

</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>App release - Masked Email Manager for Fastmail</title>
      <link>https://david.coffee/fastmail-masked-email-manager-release/</link>
      <pubDate>Wed, 19 Oct 2022 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/fastmail-masked-email-manager-release/</guid><media:content url="https://david.coffee/fastmail-masked-email-manager-release/maskedemail-app-header.jpg" medium="image" />
          <enclosure url="https://david.coffee/fastmail-masked-email-manager-release/maskedemail-app-header.jpg" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/fastmail-masked-email-manager-release/maskedemail-app-header.jpg&#34; /&gt;&lt;/p&gt;&lt;p&gt;Happy to announce that my app for managing Fastmail Masked Emails is now &lt;a href=&#34;https://apps.apple.com/us/app/masked-email-manager/id6443853807&#34;&gt;available in the AppStore&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;The app is available for free, but I might change it to the lowest price tier soon, to cover development cost (or maybe some hybrid one-time in-app purchase solution)&lt;/p&gt;
&lt;h2 id=&#34;what-it-is-and-what-it-does&#34;&gt;What it is and what it does&lt;/h2&gt;
&lt;p&gt;The app is a small utility for managing Masked Emails, Fastmail&amp;rsquo;s take on one-time / domain-specific emails.&lt;/p&gt;
&lt;p&gt;The need for this app came when I had to create Masked Emails while on the go, especially on my phone, like when signing up for a new app, or needing an email for a hotel registration. Going through the web-based Fastmail app works, but was too slow for my taste.&lt;/p&gt;
&lt;p&gt;Masked Email Manager is native and serves only that one purpose: Create and copy Masked Emails quick, with the option to disable no longer used ones.&lt;/p&gt;
&lt;h2 id=&#34;technical-implementation&#34;&gt;Technical implementation&lt;/h2&gt;
&lt;p&gt;I started this app because I wanted to do something with WASM. The core is compiled from Golang into JavaScript, then executed through React Native JSC. So it&amp;rsquo;s technically a Golang program that&amp;rsquo;s powering the main logic.&lt;/p&gt;
&lt;p&gt;Sadly after a lot of hours of debugging I came to the conclusion that WASM just isn&amp;rsquo;t feasible on React Native yet, so the Golang code is compiled to GopherJS instead.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re interested in the writeup that lead to this app, check out my other post dedicated to that: &lt;a href=&#34;https://david.coffee/react-native-wasm-golang/&#34;&gt;https://david.coffee/react-native-wasm-golang/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Honestly I had a lot of fun building this - I learned a lot about WASM and the inner-workings of React Native. It&amp;rsquo;s been a very long time since I initially used it (fun fact: I &lt;a href=&#34;https://github.com/facebook/react-native/pulls?q=is%3Apr+sort%3Aupdated-desc+author%3Advcrn+is%3Aclosed&#34;&gt;contributed to React Native in 2016&lt;/a&gt;), and it made me more motivated to work on smaller app ideas.&lt;/p&gt;
&lt;h3 id=&#34;app-logo-generated-by-dall-e&#34;&gt;App logo, generated by Dall E&lt;/h3&gt;
&lt;p&gt;I suck in designing things. I had a rough idea what I wanted to have, something along the lines of a email envelope, but with glasses. I could have probably copied together a couple of SVGs, but remembered that I had beta access to Dall E with some credits left so I gave using it a shot. Here are the results of my first prompt to Dall E:&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./maskedemaillogos.png&#34; alt=&#34;Image&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;I couldn&amp;rsquo;t be happier. I was immediately unblocked and had something decent looking. The image needed a few adjustments in Pixelmator to be fully usable, but I&amp;rsquo;m very happy with the result&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./FekYDctakAAIOsO.jpeg&#34; alt=&#34;Image&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Get the app &lt;a href=&#34;https://apps.apple.com/us/app/masked-email-manager/id6443853807&#34;&gt;on the AppStore&lt;/a&gt; and let me know what you think&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/fastmail-masked-email-manager-release/maskedemail-app-header.jpg" /></p><p>Happy to announce that my app for managing Fastmail Masked Emails is now <a href="https://apps.apple.com/us/app/masked-email-manager/id6443853807">available in the AppStore</a>!</p>
<p>The app is available for free, but I might change it to the lowest price tier soon, to cover development cost (or maybe some hybrid one-time in-app purchase solution)</p>
<h2 id="what-it-is-and-what-it-does">What it is and what it does</h2>
<p>The app is a small utility for managing Masked Emails, Fastmail&rsquo;s take on one-time / domain-specific emails.</p>
<p>The need for this app came when I had to create Masked Emails while on the go, especially on my phone, like when signing up for a new app, or needing an email for a hotel registration. Going through the web-based Fastmail app works, but was too slow for my taste.</p>
<p>Masked Email Manager is native and serves only that one purpose: Create and copy Masked Emails quick, with the option to disable no longer used ones.</p>
<h2 id="technical-implementation">Technical implementation</h2>
<p>I started this app because I wanted to do something with WASM. The core is compiled from Golang into JavaScript, then executed through React Native JSC. So it&rsquo;s technically a Golang program that&rsquo;s powering the main logic.</p>
<p>Sadly after a lot of hours of debugging I came to the conclusion that WASM just isn&rsquo;t feasible on React Native yet, so the Golang code is compiled to GopherJS instead.</p>
<p>If you&rsquo;re interested in the writeup that lead to this app, check out my other post dedicated to that: <a href="https://david.coffee/react-native-wasm-golang/">https://david.coffee/react-native-wasm-golang/</a></p>
<p>Honestly I had a lot of fun building this - I learned a lot about WASM and the inner-workings of React Native. It&rsquo;s been a very long time since I initially used it (fun fact: I <a href="https://github.com/facebook/react-native/pulls?q=is%3Apr+sort%3Aupdated-desc+author%3Advcrn+is%3Aclosed">contributed to React Native in 2016</a>), and it made me more motivated to work on smaller app ideas.</p>
<h3 id="app-logo-generated-by-dall-e">App logo, generated by Dall E</h3>
<p>I suck in designing things. I had a rough idea what I wanted to have, something along the lines of a email envelope, but with glasses. I could have probably copied together a couple of SVGs, but remembered that I had beta access to Dall E with some credits left so I gave using it a shot. Here are the results of my first prompt to Dall E:</p>
<p>
<img class="img-fluid" src="./maskedemaillogos.png" alt="Image" />

</p>
<p>I couldn&rsquo;t be happier. I was immediately unblocked and had something decent looking. The image needed a few adjustments in Pixelmator to be fully usable, but I&rsquo;m very happy with the result</p>
<p>
<img class="img-fluid" src="./FekYDctakAAIOsO.jpeg" alt="Image" />

</p>
<p>Get the app <a href="https://apps.apple.com/us/app/masked-email-manager/id6443853807">on the AppStore</a> and let me know what you think</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Venturing into WASM with React Native and Golang</title>
      <link>https://david.coffee/react-native-wasm-golang/</link>
      <pubDate>Sat, 15 Oct 2022 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/react-native-wasm-golang/</guid><media:content url="https://david.coffee/react-native-wasm-golang/goling.png" medium="image" />
          <enclosure url="https://david.coffee/react-native-wasm-golang/goling.png" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/react-native-wasm-golang/goling.png&#34; /&gt;&lt;/p&gt;&lt;p&gt;I recently had the idea to create a little react native app for FastMails MaskedEmails, to quickly add new ones on my device, so I don&amp;rsquo;t have to open the FastMail app all the time.&lt;/p&gt;
&lt;p&gt;I wanted to do this with as little code duplication as possible, so instead of rewriting a client from scratch, I decided to re-use my &lt;a href=&#34;https://github.com/dvcrn/maskedemail-cli&#34;&gt;maskedemail-cli&lt;/a&gt; somehow. I could have probably compiled it down to a shared library and pulled it into Swift, but I am no iOS developer and know React already, so why not do it in React Native with WASM?&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve never actually used wasm and was looking for an excuse to play around with it. Here&amp;rsquo;s what I learned.&lt;/p&gt;
&lt;p&gt;You can find the app I worked on during this writeup at &lt;a href=&#34;https://maskedemailmanager.david.coffee&#34;&gt;https://maskedemailmanager.david.coffee&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(Spoiler: You can&amp;rsquo;t use native WASM within JSC on React Native yet)&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;golang-and-wasm-how-does-it-actually-work&#34;&gt;Golang and WASM, how does it actually work?&lt;/h2&gt;
&lt;p&gt;Golang supports &lt;a href=&#34;https://github.com/golang/go/wiki/WebAssembly#webassembly&#34;&gt;compiling to wasm&lt;/a&gt; since version 1.11, with further improvements happening in 1.13. The basic gist of making it spit out wasm is&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;GOOS=js GOARCH=wasm go build -o main.wasm
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Main interactions from Golang with JavaScript-land happen through the &lt;code&gt;js/syscall&lt;/code&gt; &lt;a href=&#34;https://pkg.go.dev/syscall/js&#34;&gt;package&lt;/a&gt;. For example, if we want to create a js function, we can do it like so:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cb&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;FuncOf&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;this&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;args&lt;/span&gt; []&lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;any&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;fmt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Println&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Hello World&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first argument is always &lt;code&gt;this&lt;/code&gt;, and the second argument is always an array of actual arguments passed to the function.&lt;/p&gt;
&lt;p&gt;We can also use &lt;code&gt;js.Global()&lt;/code&gt; to get a ref to the global object and call into js:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Global&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;Call&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;alert&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Hello from wasm!&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&amp;hellip; and of course, we can also make our functions available on the global object:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Global&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;Set&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;myCoolFunc&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;cb&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;using-golang-wasm-from-javascript&#34;&gt;Using Golang WASM from JavaScript&lt;/h3&gt;
&lt;p&gt;Golang comes with some &lt;a href=&#34;https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.js&#34;&gt;glue code&lt;/a&gt; that you need to run wasm files built by Golang properly, that&amp;rsquo;s this copy statement you see in most guides:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;cp &amp;#34;$(go env GOROOT)/misc/wasm/wasm_exec.js&amp;#34; .
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Besides setting up the environment, this code defines the &lt;code&gt;globalThis.Go&lt;/code&gt; &lt;a href=&#34;https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.js#L95&#34;&gt;object&lt;/a&gt; that we&amp;rsquo;ll use to run our wasm code:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;go&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Go&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;WebAssembly&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;instantiateStreaming&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;main.wasm&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;go&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;importObject&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;go&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;run&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;instance&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(instantiateStreaming supports fetch; that&amp;rsquo;s why everything is so compact. we can also use &lt;code&gt;WebAssembly.instantiate(arrayBuffer, go.importObject)&lt;/code&gt; directly without fetch).&lt;/p&gt;
&lt;p&gt;&lt;code&gt;go.importObject&lt;/code&gt; provides values to be imported into the new instance, which defines &lt;a href=&#34;https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.js#L209&#34;&gt;all the supported syscalls&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;preparing-maskedemail-cli-for-wasm---async-code-and-promises&#34;&gt;Preparing maskedemail-cli for WASM - async code and Promises&lt;/h2&gt;
&lt;p&gt;First, we need to create glue code that exports the necessary functions from maskedemail-cli to JavaScript, in Golang.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;done&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; make(&lt;span style=&#34;color:#66d9ef&#34;&gt;chan&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt;{}, &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Global&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;Set&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;maskedemailList&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;FuncOf&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;list&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;-&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;list&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;this&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;args&lt;/span&gt; []&lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{} {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// (...) get maskedemails here (...)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;maskedEmailArray&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now here&amp;rsquo;s a problem: We can&amp;rsquo;t block the main goroutine with things like HTTP requests. It would be kinda weird if we do an ajax request in Golang and the browser freezes while our functions get executed&amp;hellip;&lt;/p&gt;
&lt;p&gt;Instead, async code has to go into separate goroutines, which means in JavaScript&amp;hellip; Promises!!&lt;/p&gt;
&lt;p&gt;Specifically, to make our code compatible with js promises from Golang, we have to use the Promise constructor through &lt;code&gt;js.Global()&lt;/code&gt;, effectively creating a Promise from scratch:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;list&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;this&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;args&lt;/span&gt; []&lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{} {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;handler&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;FuncOf&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;this&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;args&lt;/span&gt; []&lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{} {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;resolve&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;args&lt;/span&gt;[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    	&lt;span style=&#34;color:#a6e22e&#34;&gt;reject&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;args&lt;/span&gt;[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    	&lt;span style=&#34;color:#66d9ef&#34;&gt;go&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    	    &lt;span style=&#34;color:#75715e&#34;&gt;// (...) get maskedemails here (...)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    	    &lt;span style=&#34;color:#a6e22e&#34;&gt;resolve&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Invoke&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Hi there&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;promise&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Global&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;Get&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Promise&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;promise&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;New&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;handler&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;js.Global().Get(&amp;quot;Promise&amp;quot;)&lt;/code&gt; is getting the global Promise object, then calling &lt;code&gt;New&lt;/code&gt; onto it with a custom handler.&lt;/p&gt;
&lt;p&gt;Running &lt;code&gt;main()&lt;/code&gt; from above would now populate &lt;code&gt;globalThis.list&lt;/code&gt;, and it&amp;rsquo;s signature would look like this: &lt;code&gt;list(): Promise&amp;lt;string&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;creating-a-npm-package-thats-using-wasm&#34;&gt;Creating a NPM package that&amp;rsquo;s using WASM&lt;/h2&gt;
&lt;p&gt;Next, I wanted to publish an NPM package to pack away the complexity, but most guides on Golang + wasm were mainly focused on the browser, not NodeJS.&lt;/p&gt;
&lt;p&gt;Luckily the key steps are effectively the same:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use the &lt;code&gt;wasm_exec.js&lt;/code&gt; glue script to populate globalThis.Go&lt;/li&gt;
&lt;li&gt;Load the wasm file through whatever method&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;WebAssembly.instantiate&lt;/code&gt; or &lt;code&gt;WebAssembly.instantiateStreaming&lt;/code&gt;, pass &lt;code&gt;go.importObject&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;go.run(instance)&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In NodeJS, we can solve 1. by doing a dirty &lt;code&gt;require(&amp;quot;./wasm_exec.js&amp;quot;)&lt;/code&gt;. The code runs fine in the nodejs runtime so no issues there. Too bad it&amp;rsquo;s polluting &lt;code&gt;globalThis&lt;/code&gt; and doesn&amp;rsquo;t give us a nice-to-use package back, but we can live with that.&lt;/p&gt;
&lt;p&gt;Point 2. is a tad trickier - we could of course, just load it with &lt;code&gt;fs.readFileSync&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;go&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;globalThis&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Go&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;buffer&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fs&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;readFileSync&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;./main.wasm&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;inst&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;WebAssembly&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;instantiate&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;buffer&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;go&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;importObject&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;go&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;run&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;inst&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;instance&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// run func
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;globalThis&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;list&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&amp;hellip; but given that this is JavaScript, usage of &lt;code&gt;fs&lt;/code&gt; means that this is already unusable in a browser environment, which is not great.&lt;/p&gt;
&lt;p&gt;What&amp;rsquo;s more, &lt;code&gt;wasm_exec.js&lt;/code&gt; depends on a few browser-specific modules, mainly &lt;code&gt;crypto&lt;/code&gt; and &lt;code&gt;TextEncoder&lt;/code&gt;. So now we have some stuff that doesn&amp;rsquo;t work in the browser, and also some stuff that doesn&amp;rsquo;t work within node 😀 Let&amp;rsquo;s fix that with some polyfills:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;globalThis&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;crypto&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;crypto&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;require&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;crypto&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;globalThis&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;crypto&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;crypto&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;globalThis&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;TextEncoder&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;te&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;require&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;text-encoding-polyfill&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;globalThis&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;TextEncoder&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;te&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;TextEncoder&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;globalThis&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;TextDecoder&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;te&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;TextDecoder&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;go&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;globalThis&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Go&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;response&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;./main.wasm&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;buffer&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;response&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;arrayBuffer&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;module&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;WebAssembly&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;compile&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;buffer&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;instance&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;WebAssembly&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;instantiate&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;module&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;go&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;run&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;inst&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;instance&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// run func
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;globalThis&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;list&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Instead of &lt;code&gt;fs&lt;/code&gt;, we can use &lt;code&gt;node-fetch&lt;/code&gt; (or the new NodeJS native one) and &lt;code&gt;fetch&lt;/code&gt; to pull the wasm file, then continue instantiating like usual. Now it&amp;rsquo;s no longer node/browser or fetches implementation dependent.&lt;/p&gt;
&lt;h4 id=&#34;my-final-solution&#34;&gt;My final solution&lt;/h4&gt;
&lt;p&gt;Because I wanted to package this for ESM and CJS simultaneously, I went with &lt;a href=&#34;https://rollupjs.org/guide/en/&#34;&gt;rollup&lt;/a&gt; as bundler. &lt;a href=&#34;https://www.npmjs.com/package/@rollup/plugin-wasm&#34;&gt;rollup-plugin-wasm&lt;/a&gt; can handle loading + instantiating of the actual wasm file similar to how we did above, so we can slim the code down like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// polyfills here
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;require&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;./wasm_exec.js&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;wasmMain&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;./static/main.wasm&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;go&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;globalThis&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Go&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;wasmObj&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;wasmMain&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;go&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;importObject&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;go&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;run&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;wasmObj&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;instance&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;globalThis&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;list&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;packaging-into-a-usable-module&#34;&gt;Packaging into a usable module&lt;/h3&gt;
&lt;p&gt;To make everything a tad nicer, let&amp;rsquo;s export some functions that other modules can use:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Mod&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;list&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;string&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;accountId&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;string&lt;/span&gt;) =&amp;gt; Promise&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;MaskedEmail&lt;/span&gt;[]&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;instantiatedPromise&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Promise&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Mod&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;resolve&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;reject&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;go&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;globalThis&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;as&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;any&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;Go&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;wasmObj&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;wasmMain&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;go&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;importObject&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;go&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;run&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;wasmObj&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;instance&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;resolve&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;list&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;globalThis&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;as&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;any&lt;/span&gt;)[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;maskedemailList&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;list&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;string&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;accountId&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;string&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;mod&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;instantiatedPromise&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;mod&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;list&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;token&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;accountId&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We have some waiting to do before wasm is usable, so we package all of that into a new &lt;code&gt;instantiatedPromise&lt;/code&gt;. The exported &lt;code&gt;list()&lt;/code&gt; is then awaiting that promise before doing anything, then executing &lt;code&gt;mod.list&lt;/code&gt;, our actual wasm function.&lt;/p&gt;
&lt;p&gt;To make the API nicer to use I&amp;rsquo;ve added typescript type definitions for the module, that&amp;rsquo;s what the &lt;code&gt;interface Mod&lt;/code&gt; is for. &lt;code&gt;instantiatedPromise&lt;/code&gt; will resolve with &lt;code&gt;Mod&lt;/code&gt;, so calling &lt;code&gt;mod.list&lt;/code&gt; will have the same return signature as &lt;code&gt;Mod.list&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The package is available on &lt;a href=&#34;https://www.npmjs.com/package/maskedemail&#34;&gt;npm&lt;/a&gt;, the code on &lt;a href=&#34;https://github.com/dvcrn/maskedemail-js&#34;&gt;my github&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;investigating-react-native--wasm&#34;&gt;Investigating React Native + WASM&lt;/h2&gt;
&lt;p&gt;By now, we have packaged everything so neatly that it doesn&amp;rsquo;t matter that we used Golang to create the wasm binary. It&amp;rsquo;s all just an npm package that &lt;em&gt;should&lt;/em&gt; be usable everywhere. (Ignore the big 7MB file size of the &lt;code&gt;.wasm&lt;/code&gt; file for now 😛)&lt;/p&gt;
&lt;p&gt;So all we have to do is &lt;code&gt;yarn add maskedemail&lt;/code&gt; and that should be it&amp;hellip; right? Right??&lt;/p&gt;
&lt;p&gt;But life is not that simple, and adding that final lego piece was much harder than expected. Let&amp;rsquo;s go over what is making wasm + react native so hard?? (This was written by poking the simulator and debugging errors)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;WebAssembly.instantiateStreaming&lt;/code&gt; / &lt;code&gt;WebAssembly.compileStreaming&lt;/code&gt; only works with &lt;code&gt;fetch&lt;/code&gt; if the server is returning &lt;code&gt;application/wasm&lt;/code&gt;, which react-native doesn&amp;rsquo;t. In fact, using &lt;code&gt;fetch&lt;/code&gt; to pull in resources the same way we do in NodeJS or the Browser is in RN flimsy at best
&lt;ul&gt;
&lt;li&gt;Expo has &lt;a href=&#34;https://docs.expo.dev/versions/latest/sdk/asset/#frommodulevirtualassetmodule&#34;&gt;Asset.fromModule&lt;/a&gt; to make it possible to load arbitrary assets from disk&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WebAssembly.instantiateStreaming&lt;/code&gt; / &lt;code&gt;WebAssembly.compileStreaming&lt;/code&gt; doesn&amp;rsquo;t even exist in the react native JavaScriptCore runtime (&lt;code&gt;WebAssembly&lt;/code&gt; exists, but those functions don&amp;rsquo;t. Only &lt;code&gt;WebAssembly.instantiate&lt;/code&gt; and &lt;code&gt;WebAssembly.compile&lt;/code&gt; do, which work with ArrayBuffers&lt;/li&gt;
&lt;li&gt;Current asset loading functionality through &lt;code&gt;Asset.fromModule&lt;/code&gt; or fetch implementations for react native can pull assets, but rn&amp;rsquo;s &lt;code&gt;FileReader&lt;/code&gt; &lt;a href=&#34;https://github.com/facebook/react-native/blob/main/Libraries/Blob/FileReader.js#L77-L79&#34;&gt;does not implement readAsArrayBuffer&lt;/a&gt;, so no &lt;code&gt;fetch().then((res) =&amp;gt; res.arrayBuffer())&lt;/code&gt;. No ArrayBuffer means no easy way to use &lt;code&gt;WebAssembly.instantiate&lt;/code&gt;, and also no easy trickery to convert Blob into an ArrayBuffer&lt;/li&gt;
&lt;li&gt;RN&amp;rsquo;s JavaScriptCore does not have a &lt;code&gt;crypto&lt;/code&gt; implementation&lt;/li&gt;
&lt;li&gt;Expos packager is trying to resolve all &lt;code&gt;require&lt;/code&gt; and build and doesn&amp;rsquo;t support dynamic imports, so having &lt;code&gt;require(&amp;quot;crypto&amp;quot;)&lt;/code&gt; in an if statement (like a env-dependent polyfill) will &lt;em&gt;still&lt;/em&gt; make the packager throw an error that the package doesn&amp;rsquo;t exist, effectively meaning that our generic solution ain&amp;rsquo;t gonna work&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I probably forgot a few points but these were the most noticeable issues.&lt;/p&gt;
&lt;p&gt;I banged my head against &lt;code&gt;FileReader.readAsArrayBuffer&lt;/code&gt; to make it possible to read the &lt;code&gt;.wasm&lt;/code&gt; file from disk, tried a couple polyfills and re-implementations, but eventually gave up. There are &lt;code&gt;fs.readFile&lt;/code&gt; implementations that could work, but expo (the tool I am using to bootstrap my RN application) &lt;em&gt;does not support&lt;/em&gt; linking libraries, so we can&amp;rsquo;t easily add things that require editing of the application bundle.&lt;/p&gt;
&lt;p&gt;To make things worse, the package resolving the issue I mentioned above also meant that all the glue code that &lt;code&gt;rollup-plugin-wasm&lt;/code&gt; generated to switch based on environment for browser + nodejs doesn&amp;rsquo;t work either, because if there is a call to &lt;code&gt;require(&amp;quot;fs&amp;quot;)&lt;/code&gt;, expo breaks. Same thing for attempts to polyfill &lt;code&gt;crypto&lt;/code&gt; in the correct environment with &lt;code&gt;crypto-browserify.&lt;/code&gt;&lt;/p&gt;
&lt;h3 id=&#34;fixing-rn-issues-one-by-one&#34;&gt;Fixing RN issues one by one&lt;/h3&gt;
&lt;p&gt;Looking at &lt;code&gt;wasm_exec.js&lt;/code&gt;, we can see that it doesn&amp;rsquo;t need the entire &lt;code&gt;crypto&lt;/code&gt; package, just one single function:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;globalThis&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;crypto&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Error(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s much easier to polyfill with something like &lt;a href=&#34;https://github.com/kumavis/polyfill-crypto.getrandomvalues&#34;&gt;polyfill-crypto.getrandomvalues&lt;/a&gt; (though using &lt;code&gt;Math.random()&lt;/code&gt; instead of proper entropy source&amp;hellip;)&lt;/p&gt;
&lt;p&gt;We can swap out our existing polyfill for this implementation:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;getRandomValues&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;polyfill-crypto.getrandomvalues&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;globalThis&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;crypto&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;globalThis&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;crypto&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;getRandomValues&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  };
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now the import no longer breaks nodejs, browser, or react-native JavaScriptCore :)&lt;/p&gt;
&lt;p&gt;Loading the &lt;code&gt;.wasm&lt;/code&gt; file is a different beast, and the polyfills for &lt;code&gt;FileReader.readAsArrayBuffer&lt;/code&gt; caused expo to crash consistently. So, what do we do if we can&amp;rsquo;t load something from disk in the format we need? We play dirty: Instead of async loading the &lt;code&gt;.wasm&lt;/code&gt; file like a good citizen, we base64 that chunker and stuff it into our main JavaScript file!&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rollup-plugin-wasm&lt;/code&gt; even has an option to inline wasm:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;wasm&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;sync&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;main.wasm&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;targetEnv&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;auto-inline&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;maxFileSize&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}),
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The result will make any editor without proper optimizations for uber-large files crawl&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;https://david.coffee/assets/wasm-base64.png&#34; alt=&#34;Delicious base64 blobs&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;&amp;hellip; but it does work inside the Simulator! We&amp;rsquo;re no longer dependent on asset loading logic anymore. It&amp;rsquo;s all just js now.&lt;/p&gt;
&lt;p&gt;To fix remaining &lt;code&gt;FileLoader&lt;/code&gt; issues, there is &lt;a href=&#34;https://github.com/acostalima/react-native-polyfill-globals/blob/master/patches/react-native%2B0.63.3.patch&#34;&gt;this patch&lt;/a&gt; available that polyfills the functionality. This patch alone doesn&amp;rsquo;t work to load the wasm file from disk and crashes the simulator, but it does work for smaller under-the-hood functionality that relies on &lt;code&gt;readAsArrayBuffer&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;With that out of the way, my app finally runs in the Simulator 🎉&lt;/p&gt;
&lt;p&gt;I thought this would be the end of the story and continued chipping away on my app. After days of work, it was finally ready to run it on my iPhone to do a final test before release. Compiling everything&amp;hellip; running&amp;hellip;&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;https://david.coffee/assets/Screen%20Shot%202022-10-02%20at%2023.14.08.png&#34; alt=&#34;Screen Shot 2022-10-02 at 23.14.08&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;You gotta be kidding. &lt;code&gt;WebAssembly&lt;/code&gt; does not exist at all in JavaScriptCore when running on an actual device, but it &lt;em&gt;does&lt;/em&gt; exist inside the Simulator. Apparently, Apple shut it down in recent iOS versions, but from reading through posts, this used to be available in the past.&lt;/p&gt;
&lt;h2 id=&#34;-the-end-did-we-fail-what-next&#34;&gt;&amp;hellip; the end? Did we fail? What next?&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been considering what next steps to take to get this working, but without proper WebAssembly support in JSC on devices, performance will always be subpar. Here are some thoughts and directions I investigated:&lt;/p&gt;
&lt;h4 id=&#34;option-1-fallback-to-wkwebview&#34;&gt;Option 1: Fallback to WKWebView&lt;/h4&gt;
&lt;p&gt;This is the most promising but also a bit absurd. We create a WebView, inject our wasm binary into the webview and execute it there, then relay messages (with &lt;code&gt;window.postMessage&lt;/code&gt;) back and forth with our main app.&lt;/p&gt;
&lt;p&gt;In my tests, this actually worked pretty well, with the problem that making something generic that &lt;em&gt;just works&lt;/em&gt; is a bit hard. Actually, polyfilling &lt;code&gt;window.WebAssembly&lt;/code&gt; would mean we need to somehow bridge &lt;code&gt;go.importObject&lt;/code&gt; into our webview.
On top of that, we need to proxy the changes the wasm binary did (like modifying globalThis) back to our native app using a transparent message relay, so libraries that rely on it can &lt;em&gt;just work&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Without a proper polyfill, &lt;code&gt;window.WebAssembly&lt;/code&gt; won&amp;rsquo;t be available in global scope, so our &lt;code&gt;maskedemail&lt;/code&gt; NPM package won&amp;rsquo;t be able to use it.&lt;/p&gt;
&lt;p&gt;All in all, a lot of headache. Probably better to split native and bridged code altogether and write the glue code from scratch. (&lt;a href=&#34;https://github.com/inokawa/react-native-react-bridge&#34;&gt;react-native-react-bridge&lt;/a&gt; tries to get around this by using a separate entrypoint that&amp;rsquo;s getting processed with babel)&lt;/p&gt;
&lt;h4 id=&#34;option-2-wasm2js&#34;&gt;Option 2: wasm2js&lt;/h4&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/WebAssembly/binaryen&#34;&gt;binaryen&lt;/a&gt; includes a tool called &lt;code&gt;wasm2js&lt;/code&gt; that compiles wasm files back to JavaScript. I have tried this as well, but could not get it running with my library, probably due to complexity with some stuff like reflections and HTTP requests. Very promising but very hard to debug due to the way this tool spits out JavaScript. Either it works, or it doesn&amp;rsquo;t; for me, it didn&amp;rsquo;t. (Though the errors I got were actual Go errors related to HTTP, so it almost worked)&lt;/p&gt;
&lt;h4 id=&#34;option-3-gopherjs&#34;&gt;Option 3: gopherjs&lt;/h4&gt;
&lt;p&gt;Not a solution to wasm, but to make Go run within react-native. Instead of building a wasm binary, &lt;a href=&#34;https://github.com/gopherjs/gopherjs&#34;&gt;gopherjs&lt;/a&gt; can output functional JavaScript so we don&amp;rsquo;t have to deal with WebAssembly runtime at all.&lt;/p&gt;
&lt;p&gt;Honestly, I think this might be the best option for code reusability until WebAssembly support is fully available in JSC.&lt;/p&gt;
&lt;h2 id=&#34;what-i-ended-up-doing-wasm---gopherjs&#34;&gt;What I ended up doing: WASM -&amp;gt; GopherJS&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve tried all options listed above, and the GopherJS variant was the nicest to work with. Even better: The Go API for GopherJS is almost identical to WASM! For example:&lt;/p&gt;
&lt;p&gt;This is the WASM version:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;list&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;this&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;args&lt;/span&gt; []&lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{} {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;handler&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;FuncOf&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;this&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;args&lt;/span&gt; []&lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Value&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{} {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;resolve&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;args&lt;/span&gt;[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    	&lt;span style=&#34;color:#a6e22e&#34;&gt;reject&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;args&lt;/span&gt;[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    	&lt;span style=&#34;color:#66d9ef&#34;&gt;go&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    	    &lt;span style=&#34;color:#75715e&#34;&gt;// (...) get maskedemails here (...)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    	    &lt;span style=&#34;color:#a6e22e&#34;&gt;resolve&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Invoke&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Hi there&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;promiseConstructor&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Global&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;Get&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Promise&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;promiseConstructor&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;New&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;handler&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is the gopherjs version:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;list&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;this&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Object&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;args&lt;/span&gt; []&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Object&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{} {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;handler&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;MakeFunc&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;this&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Object&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;args&lt;/span&gt; []&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Object&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt;{} {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;resolve&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;args&lt;/span&gt;[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;reject&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;args&lt;/span&gt;[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;go&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        	&lt;span style=&#34;color:#75715e&#34;&gt;// (...) get maskedemails here (...)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#a6e22e&#34;&gt;resolve&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Invoke&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;out&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		}()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;promiseConstructor&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;js&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Global&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Get&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Promise&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;promiseConstructor&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;New&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;handler&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In fact, it&amp;rsquo;s so similar that I was able to flat-out generate the &lt;code&gt;gopherjs&lt;/code&gt; main.go file with this script:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-makefile&#34; data-lang=&#34;makefile&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;.PHONY&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; generate-gopherjs
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;generate-gopherjs&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	rm -rf cmd/gopherjs
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	mkdir -p cmd/gopherjs
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	cp main.go cmd/gopherjs/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	sed -i &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;s/syscall\/js/github\.com\/gopherjs\/gopherjs\/js/g&amp;#39;&lt;/span&gt; cmd/gopherjs/main.go
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	sed -i &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;s/FuncOf/MakeFunc/g&amp;#39;&lt;/span&gt; cmd/gopherjs/main.go
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	sed -i &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;s/js\.Value/\*js\.Object/g&amp;#39;&lt;/span&gt; cmd/gopherjs/main.go
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	sed -i &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;s/js\.Global()/js\.Global/g&amp;#39;&lt;/span&gt; cmd/gopherjs/main.go
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With a bit of JavaScript, I was able to add an automatic fallback to the gopherjs version to &lt;code&gt;maskedemail-js&lt;/code&gt;, so it&amp;rsquo;s now usable even when &lt;code&gt;window.WebAssembly&lt;/code&gt; isn&amp;rsquo;t available (although ships with 2x 8MB files).&lt;/p&gt;
&lt;h2 id=&#34;no-wasm-in-react-native-as-of-now&#34;&gt;No WASM in react-native as of now&lt;/h2&gt;
&lt;p&gt;A bit sad I wasn&amp;rsquo;t able to get my goal of using real WASM working within React Native, but learned a good bit with this adventure.&lt;/p&gt;
&lt;p&gt;For now, I&amp;rsquo;ll use GopherJS when I want to re-use Go code, but I hope we&amp;rsquo;ll see actual WebAssembly support in JSC on devices soon-ish. With all the WASM hype, I doubt it&amp;rsquo;ll take long.&lt;/p&gt;
&lt;p&gt;There is also stuff like &lt;a href=&#34;https://wasmer.io&#34;&gt;Wasmer&lt;/a&gt; that can embed WASM binaries into other languages like Swift, so instead of going the JSC route, it should be possible to embed it into native modules directly or non-RN apps - for when we really want to re-use the same binary at different places. But that&amp;rsquo;s also doable with Golang, without WASM.&lt;/p&gt;
&lt;p&gt;The app is available at &lt;a href=&#34;https://maskedemailmanager.david.coffee&#34;&gt;https://maskedemailmanager.david.coffee&lt;/a&gt;&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/react-native-wasm-golang/goling.png" /></p><p>I recently had the idea to create a little react native app for FastMails MaskedEmails, to quickly add new ones on my device, so I don&rsquo;t have to open the FastMail app all the time.</p>
<p>I wanted to do this with as little code duplication as possible, so instead of rewriting a client from scratch, I decided to re-use my <a href="https://github.com/dvcrn/maskedemail-cli">maskedemail-cli</a> somehow. I could have probably compiled it down to a shared library and pulled it into Swift, but I am no iOS developer and know React already, so why not do it in React Native with WASM?</p>
<p>I&rsquo;ve never actually used wasm and was looking for an excuse to play around with it. Here&rsquo;s what I learned.</p>
<p>You can find the app I worked on during this writeup at <a href="https://maskedemailmanager.david.coffee">https://maskedemailmanager.david.coffee</a></p>
<p><em>(Spoiler: You can&rsquo;t use native WASM within JSC on React Native yet)</em></p>
<h2 id="golang-and-wasm-how-does-it-actually-work">Golang and WASM, how does it actually work?</h2>
<p>Golang supports <a href="https://github.com/golang/go/wiki/WebAssembly#webassembly">compiling to wasm</a> since version 1.11, with further improvements happening in 1.13. The basic gist of making it spit out wasm is</p>
<pre tabindex="0"><code>GOOS=js GOARCH=wasm go build -o main.wasm
</code></pre><p>Main interactions from Golang with JavaScript-land happen through the <code>js/syscall</code> <a href="https://pkg.go.dev/syscall/js">package</a>. For example, if we want to create a js function, we can do it like so:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">cb</span> = <span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">FuncOf</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">this</span> <span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Value</span>, <span style="color:#a6e22e">args</span> []<span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Value</span>) <span style="color:#66d9ef">any</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;Hello World&#34;</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><p>The first argument is always <code>this</code>, and the second argument is always an array of actual arguments passed to the function.</p>
<p>We can also use <code>js.Global()</code> to get a ref to the global object and call into js:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Global</span>().<span style="color:#a6e22e">Call</span>(<span style="color:#e6db74">&#34;alert&#34;</span>, <span style="color:#e6db74">&#34;Hello from wasm!&#34;</span>)
</span></span></code></pre></div><p>&hellip; and of course, we can also make our functions available on the global object:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Global</span>().<span style="color:#a6e22e">Set</span>(<span style="color:#e6db74">&#34;myCoolFunc&#34;</span>, <span style="color:#a6e22e">cb</span>)
</span></span></code></pre></div><h3 id="using-golang-wasm-from-javascript">Using Golang WASM from JavaScript</h3>
<p>Golang comes with some <a href="https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.js">glue code</a> that you need to run wasm files built by Golang properly, that&rsquo;s this copy statement you see in most guides:</p>
<pre tabindex="0"><code>cp &#34;$(go env GOROOT)/misc/wasm/wasm_exec.js&#34; .
</code></pre><p>Besides setting up the environment, this code defines the <code>globalThis.Go</code> <a href="https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.js#L95">object</a> that we&rsquo;ll use to run our wasm code:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">go</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Go</span>();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">res</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">WebAssembly</span>.<span style="color:#a6e22e">instantiateStreaming</span>(
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">fetch</span>(<span style="color:#e6db74">&#34;main.wasm&#34;</span>),
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">go</span>.<span style="color:#a6e22e">importObject</span>,
</span></span><span style="display:flex;"><span>);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">go</span>.<span style="color:#a6e22e">run</span>(<span style="color:#a6e22e">res</span>.<span style="color:#a6e22e">instance</span>);
</span></span></code></pre></div><p>(instantiateStreaming supports fetch; that&rsquo;s why everything is so compact. we can also use <code>WebAssembly.instantiate(arrayBuffer, go.importObject)</code> directly without fetch).</p>
<p><code>go.importObject</code> provides values to be imported into the new instance, which defines <a href="https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.js#L209">all the supported syscalls</a></p>
<h2 id="preparing-maskedemail-cli-for-wasm---async-code-and-promises">Preparing maskedemail-cli for WASM - async code and Promises</h2>
<p>First, we need to create glue code that exports the necessary functions from maskedemail-cli to JavaScript, in Golang.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">done</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">struct</span>{}, <span style="color:#ae81ff">0</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Global</span>().<span style="color:#a6e22e">Set</span>(<span style="color:#e6db74">&#34;maskedemailList&#34;</span>, <span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">FuncOf</span>(<span style="color:#a6e22e">list</span>))
</span></span><span style="display:flex;"><span>	<span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">done</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">list</span>(<span style="color:#a6e22e">this</span> <span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Value</span>, <span style="color:#a6e22e">args</span> []<span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Value</span>) <span style="color:#66d9ef">interface</span>{} {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// (...) get maskedemails here (...)</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">maskedEmailArray</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now here&rsquo;s a problem: We can&rsquo;t block the main goroutine with things like HTTP requests. It would be kinda weird if we do an ajax request in Golang and the browser freezes while our functions get executed&hellip;</p>
<p>Instead, async code has to go into separate goroutines, which means in JavaScript&hellip; Promises!!</p>
<p>Specifically, to make our code compatible with js promises from Golang, we have to use the Promise constructor through <code>js.Global()</code>, effectively creating a Promise from scratch:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">list</span>(<span style="color:#a6e22e">this</span> <span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Value</span>, <span style="color:#a6e22e">args</span> []<span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Value</span>) <span style="color:#66d9ef">interface</span>{} {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">handler</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">FuncOf</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">this</span> <span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Value</span>, <span style="color:#a6e22e">args</span> []<span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Value</span>) <span style="color:#66d9ef">interface</span>{} {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">resolve</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">0</span>]
</span></span><span style="display:flex;"><span>    	<span style="color:#a6e22e">reject</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">1</span>]
</span></span><span style="display:flex;"><span>    	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>    	    <span style="color:#75715e">// (...) get maskedemails here (...)</span>
</span></span><span style="display:flex;"><span>    	    <span style="color:#a6e22e">resolve</span>.<span style="color:#a6e22e">Invoke</span>(<span style="color:#e6db74">&#34;Hi there&#34;</span>)
</span></span><span style="display:flex;"><span>    	}
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">promise</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Global</span>().<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;Promise&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">promise</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">handler</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>js.Global().Get(&quot;Promise&quot;)</code> is getting the global Promise object, then calling <code>New</code> onto it with a custom handler.</p>
<p>Running <code>main()</code> from above would now populate <code>globalThis.list</code>, and it&rsquo;s signature would look like this: <code>list(): Promise&lt;string&gt;</code>.</p>
<h2 id="creating-a-npm-package-thats-using-wasm">Creating a NPM package that&rsquo;s using WASM</h2>
<p>Next, I wanted to publish an NPM package to pack away the complexity, but most guides on Golang + wasm were mainly focused on the browser, not NodeJS.</p>
<p>Luckily the key steps are effectively the same:</p>
<ol>
<li>Use the <code>wasm_exec.js</code> glue script to populate globalThis.Go</li>
<li>Load the wasm file through whatever method</li>
<li>Run <code>WebAssembly.instantiate</code> or <code>WebAssembly.instantiateStreaming</code>, pass <code>go.importObject</code></li>
<li>Run <code>go.run(instance)</code></li>
</ol>
<p>In NodeJS, we can solve 1. by doing a dirty <code>require(&quot;./wasm_exec.js&quot;)</code>. The code runs fine in the nodejs runtime so no issues there. Too bad it&rsquo;s polluting <code>globalThis</code> and doesn&rsquo;t give us a nice-to-use package back, but we can live with that.</p>
<p>Point 2. is a tad trickier - we could of course, just load it with <code>fs.readFileSync</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">go</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">globalThis</span>.<span style="color:#a6e22e">Go</span>();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">buffer</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">fs</span>.<span style="color:#a6e22e">readFileSync</span>(<span style="color:#e6db74">&#34;./main.wasm&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">inst</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">WebAssembly</span>.<span style="color:#a6e22e">instantiate</span>(<span style="color:#a6e22e">buffer</span>, <span style="color:#a6e22e">go</span>.<span style="color:#a6e22e">importObject</span>);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">go</span>.<span style="color:#a6e22e">run</span>(<span style="color:#a6e22e">inst</span>.<span style="color:#a6e22e">instance</span>);
</span></span><span style="display:flex;"><span><span style="color:#75715e">// run func
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">globalThis</span>.<span style="color:#a6e22e">list</span>();
</span></span></code></pre></div><p>&hellip; but given that this is JavaScript, usage of <code>fs</code> means that this is already unusable in a browser environment, which is not great.</p>
<p>What&rsquo;s more, <code>wasm_exec.js</code> depends on a few browser-specific modules, mainly <code>crypto</code> and <code>TextEncoder</code>. So now we have some stuff that doesn&rsquo;t work in the browser, and also some stuff that doesn&rsquo;t work within node 😀 Let&rsquo;s fix that with some polyfills:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">globalThis</span>.<span style="color:#a6e22e">crypto</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">crypto</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">require</span>(<span style="color:#e6db74">&#34;crypto&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">globalThis</span>.<span style="color:#a6e22e">crypto</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">crypto</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">globalThis</span>.<span style="color:#a6e22e">TextEncoder</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">te</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">require</span>(<span style="color:#e6db74">&#34;text-encoding-polyfill&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">globalThis</span>.<span style="color:#a6e22e">TextEncoder</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">te</span>.<span style="color:#a6e22e">TextEncoder</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">globalThis</span>.<span style="color:#a6e22e">TextDecoder</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">te</span>.<span style="color:#a6e22e">TextDecoder</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">go</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">globalThis</span>.<span style="color:#a6e22e">Go</span>();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">response</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fetch</span>(<span style="color:#e6db74">&#34;./main.wasm&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">buffer</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">arrayBuffer</span>();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">module</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">WebAssembly</span>.<span style="color:#a6e22e">compile</span>(<span style="color:#a6e22e">buffer</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">instance</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">WebAssembly</span>.<span style="color:#a6e22e">instantiate</span>(<span style="color:#a6e22e">module</span>);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">go</span>.<span style="color:#a6e22e">run</span>(<span style="color:#a6e22e">inst</span>.<span style="color:#a6e22e">instance</span>);
</span></span><span style="display:flex;"><span><span style="color:#75715e">// run func
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">globalThis</span>.<span style="color:#a6e22e">list</span>();
</span></span></code></pre></div><p>Instead of <code>fs</code>, we can use <code>node-fetch</code> (or the new NodeJS native one) and <code>fetch</code> to pull the wasm file, then continue instantiating like usual. Now it&rsquo;s no longer node/browser or fetches implementation dependent.</p>
<h4 id="my-final-solution">My final solution</h4>
<p>Because I wanted to package this for ESM and CJS simultaneously, I went with <a href="https://rollupjs.org/guide/en/">rollup</a> as bundler. <a href="https://www.npmjs.com/package/@rollup/plugin-wasm">rollup-plugin-wasm</a> can handle loading + instantiating of the actual wasm file similar to how we did above, so we can slim the code down like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#75715e">// polyfills here
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">require</span>(<span style="color:#e6db74">&#34;./wasm_exec.js&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">wasmMain</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;./static/main.wasm&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">go</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">globalThis</span>.<span style="color:#a6e22e">Go</span>();
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">wasmObj</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">wasmMain</span>(<span style="color:#a6e22e">go</span>.<span style="color:#a6e22e">importObject</span>);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">go</span>.<span style="color:#a6e22e">run</span>(<span style="color:#a6e22e">wasmObj</span>.<span style="color:#a6e22e">instance</span>);
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">globalThis</span>.<span style="color:#a6e22e">list</span>();
</span></span></code></pre></div><h3 id="packaging-into-a-usable-module">Packaging into a usable module</h3>
<p>To make everything a tad nicer, let&rsquo;s export some functions that other modules can use:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">Mod</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">list</span><span style="color:#f92672">:</span> (<span style="color:#a6e22e">token</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>, <span style="color:#a6e22e">accountId</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>) =&gt; Promise<span style="color:#f92672">&lt;</span><span style="color:#a6e22e">MaskedEmail</span>[]<span style="color:#f92672">&gt;</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">instantiatedPromise</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> Promise<span style="color:#f92672">&lt;</span><span style="color:#a6e22e">Mod</span><span style="color:#f92672">&gt;</span>(<span style="color:#66d9ef">async</span> (<span style="color:#a6e22e">resolve</span>, <span style="color:#a6e22e">reject</span>) =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">go</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> (<span style="color:#a6e22e">globalThis</span> <span style="color:#a6e22e">as</span> <span style="color:#a6e22e">any</span>).<span style="color:#a6e22e">Go</span>();
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">wasmObj</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">wasmMain</span>(<span style="color:#a6e22e">go</span>.<span style="color:#a6e22e">importObject</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">go</span>.<span style="color:#a6e22e">run</span>(<span style="color:#a6e22e">wasmObj</span>.<span style="color:#a6e22e">instance</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">resolve</span>({
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">list</span><span style="color:#f92672">:</span> (<span style="color:#a6e22e">globalThis</span> <span style="color:#a6e22e">as</span> <span style="color:#a6e22e">any</span>)[<span style="color:#e6db74">&#34;maskedemailList&#34;</span>],
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">list</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">async</span> (<span style="color:#a6e22e">token</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>, <span style="color:#a6e22e">accountId</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span>) =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">mod</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">instantiatedPromise</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">mod</span>.<span style="color:#a6e22e">list</span>(<span style="color:#a6e22e">token</span>, <span style="color:#a6e22e">accountId</span>);
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>We have some waiting to do before wasm is usable, so we package all of that into a new <code>instantiatedPromise</code>. The exported <code>list()</code> is then awaiting that promise before doing anything, then executing <code>mod.list</code>, our actual wasm function.</p>
<p>To make the API nicer to use I&rsquo;ve added typescript type definitions for the module, that&rsquo;s what the <code>interface Mod</code> is for. <code>instantiatedPromise</code> will resolve with <code>Mod</code>, so calling <code>mod.list</code> will have the same return signature as <code>Mod.list</code></p>
<p>The package is available on <a href="https://www.npmjs.com/package/maskedemail">npm</a>, the code on <a href="https://github.com/dvcrn/maskedemail-js">my github</a></p>
<h2 id="investigating-react-native--wasm">Investigating React Native + WASM</h2>
<p>By now, we have packaged everything so neatly that it doesn&rsquo;t matter that we used Golang to create the wasm binary. It&rsquo;s all just an npm package that <em>should</em> be usable everywhere. (Ignore the big 7MB file size of the <code>.wasm</code> file for now 😛)</p>
<p>So all we have to do is <code>yarn add maskedemail</code> and that should be it&hellip; right? Right??</p>
<p>But life is not that simple, and adding that final lego piece was much harder than expected. Let&rsquo;s go over what is making wasm + react native so hard?? (This was written by poking the simulator and debugging errors)</p>
<ul>
<li><code>WebAssembly.instantiateStreaming</code> / <code>WebAssembly.compileStreaming</code> only works with <code>fetch</code> if the server is returning <code>application/wasm</code>, which react-native doesn&rsquo;t. In fact, using <code>fetch</code> to pull in resources the same way we do in NodeJS or the Browser is in RN flimsy at best
<ul>
<li>Expo has <a href="https://docs.expo.dev/versions/latest/sdk/asset/#frommodulevirtualassetmodule">Asset.fromModule</a> to make it possible to load arbitrary assets from disk</li>
</ul>
</li>
<li><code>WebAssembly.instantiateStreaming</code> / <code>WebAssembly.compileStreaming</code> doesn&rsquo;t even exist in the react native JavaScriptCore runtime (<code>WebAssembly</code> exists, but those functions don&rsquo;t. Only <code>WebAssembly.instantiate</code> and <code>WebAssembly.compile</code> do, which work with ArrayBuffers</li>
<li>Current asset loading functionality through <code>Asset.fromModule</code> or fetch implementations for react native can pull assets, but rn&rsquo;s <code>FileReader</code> <a href="https://github.com/facebook/react-native/blob/main/Libraries/Blob/FileReader.js#L77-L79">does not implement readAsArrayBuffer</a>, so no <code>fetch().then((res) =&gt; res.arrayBuffer())</code>. No ArrayBuffer means no easy way to use <code>WebAssembly.instantiate</code>, and also no easy trickery to convert Blob into an ArrayBuffer</li>
<li>RN&rsquo;s JavaScriptCore does not have a <code>crypto</code> implementation</li>
<li>Expos packager is trying to resolve all <code>require</code> and build and doesn&rsquo;t support dynamic imports, so having <code>require(&quot;crypto&quot;)</code> in an if statement (like a env-dependent polyfill) will <em>still</em> make the packager throw an error that the package doesn&rsquo;t exist, effectively meaning that our generic solution ain&rsquo;t gonna work</li>
</ul>
<p>I probably forgot a few points but these were the most noticeable issues.</p>
<p>I banged my head against <code>FileReader.readAsArrayBuffer</code> to make it possible to read the <code>.wasm</code> file from disk, tried a couple polyfills and re-implementations, but eventually gave up. There are <code>fs.readFile</code> implementations that could work, but expo (the tool I am using to bootstrap my RN application) <em>does not support</em> linking libraries, so we can&rsquo;t easily add things that require editing of the application bundle.</p>
<p>To make things worse, the package resolving the issue I mentioned above also meant that all the glue code that <code>rollup-plugin-wasm</code> generated to switch based on environment for browser + nodejs doesn&rsquo;t work either, because if there is a call to <code>require(&quot;fs&quot;)</code>, expo breaks. Same thing for attempts to polyfill <code>crypto</code> in the correct environment with <code>crypto-browserify.</code></p>
<h3 id="fixing-rn-issues-one-by-one">Fixing RN issues one by one</h3>
<p>Looking at <code>wasm_exec.js</code>, we can see that it doesn&rsquo;t need the entire <code>crypto</code> package, just one single function:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">globalThis</span>.<span style="color:#a6e22e">crypto</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> Error(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)&#34;</span>,
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That&rsquo;s much easier to polyfill with something like <a href="https://github.com/kumavis/polyfill-crypto.getrandomvalues">polyfill-crypto.getrandomvalues</a> (though using <code>Math.random()</code> instead of proper entropy source&hellip;)</p>
<p>We can swap out our existing polyfill for this implementation:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">getRandomValues</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#34;polyfill-crypto.getrandomvalues&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">globalThis</span>.<span style="color:#a6e22e">crypto</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">globalThis</span>.<span style="color:#a6e22e">crypto</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">getRandomValues</span>,
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now the import no longer breaks nodejs, browser, or react-native JavaScriptCore :)</p>
<p>Loading the <code>.wasm</code> file is a different beast, and the polyfills for <code>FileReader.readAsArrayBuffer</code> caused expo to crash consistently. So, what do we do if we can&rsquo;t load something from disk in the format we need? We play dirty: Instead of async loading the <code>.wasm</code> file like a good citizen, we base64 that chunker and stuff it into our main JavaScript file!</p>
<p><code>rollup-plugin-wasm</code> even has an option to inline wasm:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#a6e22e">wasm</span>({
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">sync</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#34;main.wasm&#34;</span>],
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">targetEnv</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;auto-inline&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">maxFileSize</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>}),
</span></span></code></pre></div><p>The result will make any editor without proper optimizations for uber-large files crawl</p>
<p>
<img class="img-fluid" src="https://david.coffee/assets/wasm-base64.png" alt="Delicious base64 blobs" />

</p>
<p>&hellip; but it does work inside the Simulator! We&rsquo;re no longer dependent on asset loading logic anymore. It&rsquo;s all just js now.</p>
<p>To fix remaining <code>FileLoader</code> issues, there is <a href="https://github.com/acostalima/react-native-polyfill-globals/blob/master/patches/react-native%2B0.63.3.patch">this patch</a> available that polyfills the functionality. This patch alone doesn&rsquo;t work to load the wasm file from disk and crashes the simulator, but it does work for smaller under-the-hood functionality that relies on <code>readAsArrayBuffer</code>.</p>
<p>With that out of the way, my app finally runs in the Simulator 🎉</p>
<p>I thought this would be the end of the story and continued chipping away on my app. After days of work, it was finally ready to run it on my iPhone to do a final test before release. Compiling everything&hellip; running&hellip;</p>
<p>
<img class="img-fluid" src="https://david.coffee/assets/Screen%20Shot%202022-10-02%20at%2023.14.08.png" alt="Screen Shot 2022-10-02 at 23.14.08" />

</p>
<p>You gotta be kidding. <code>WebAssembly</code> does not exist at all in JavaScriptCore when running on an actual device, but it <em>does</em> exist inside the Simulator. Apparently, Apple shut it down in recent iOS versions, but from reading through posts, this used to be available in the past.</p>
<h2 id="-the-end-did-we-fail-what-next">&hellip; the end? Did we fail? What next?</h2>
<p>I&rsquo;ve been considering what next steps to take to get this working, but without proper WebAssembly support in JSC on devices, performance will always be subpar. Here are some thoughts and directions I investigated:</p>
<h4 id="option-1-fallback-to-wkwebview">Option 1: Fallback to WKWebView</h4>
<p>This is the most promising but also a bit absurd. We create a WebView, inject our wasm binary into the webview and execute it there, then relay messages (with <code>window.postMessage</code>) back and forth with our main app.</p>
<p>In my tests, this actually worked pretty well, with the problem that making something generic that <em>just works</em> is a bit hard. Actually, polyfilling <code>window.WebAssembly</code> would mean we need to somehow bridge <code>go.importObject</code> into our webview.
On top of that, we need to proxy the changes the wasm binary did (like modifying globalThis) back to our native app using a transparent message relay, so libraries that rely on it can <em>just work</em>.</p>
<p>Without a proper polyfill, <code>window.WebAssembly</code> won&rsquo;t be available in global scope, so our <code>maskedemail</code> NPM package won&rsquo;t be able to use it.</p>
<p>All in all, a lot of headache. Probably better to split native and bridged code altogether and write the glue code from scratch. (<a href="https://github.com/inokawa/react-native-react-bridge">react-native-react-bridge</a> tries to get around this by using a separate entrypoint that&rsquo;s getting processed with babel)</p>
<h4 id="option-2-wasm2js">Option 2: wasm2js</h4>
<p><a href="https://github.com/WebAssembly/binaryen">binaryen</a> includes a tool called <code>wasm2js</code> that compiles wasm files back to JavaScript. I have tried this as well, but could not get it running with my library, probably due to complexity with some stuff like reflections and HTTP requests. Very promising but very hard to debug due to the way this tool spits out JavaScript. Either it works, or it doesn&rsquo;t; for me, it didn&rsquo;t. (Though the errors I got were actual Go errors related to HTTP, so it almost worked)</p>
<h4 id="option-3-gopherjs">Option 3: gopherjs</h4>
<p>Not a solution to wasm, but to make Go run within react-native. Instead of building a wasm binary, <a href="https://github.com/gopherjs/gopherjs">gopherjs</a> can output functional JavaScript so we don&rsquo;t have to deal with WebAssembly runtime at all.</p>
<p>Honestly, I think this might be the best option for code reusability until WebAssembly support is fully available in JSC.</p>
<h2 id="what-i-ended-up-doing-wasm---gopherjs">What I ended up doing: WASM -&gt; GopherJS</h2>
<p>I&rsquo;ve tried all options listed above, and the GopherJS variant was the nicest to work with. Even better: The Go API for GopherJS is almost identical to WASM! For example:</p>
<p>This is the WASM version:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">list</span>(<span style="color:#a6e22e">this</span> <span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Value</span>, <span style="color:#a6e22e">args</span> []<span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Value</span>) <span style="color:#66d9ef">interface</span>{} {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">handler</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">FuncOf</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">this</span> <span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Value</span>, <span style="color:#a6e22e">args</span> []<span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Value</span>) <span style="color:#66d9ef">interface</span>{} {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">resolve</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">0</span>]
</span></span><span style="display:flex;"><span>    	<span style="color:#a6e22e">reject</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">1</span>]
</span></span><span style="display:flex;"><span>    	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>    	    <span style="color:#75715e">// (...) get maskedemails here (...)</span>
</span></span><span style="display:flex;"><span>    	    <span style="color:#a6e22e">resolve</span>.<span style="color:#a6e22e">Invoke</span>(<span style="color:#e6db74">&#34;Hi there&#34;</span>)
</span></span><span style="display:flex;"><span>    	}
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">promiseConstructor</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Global</span>().<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;Promise&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">promiseConstructor</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">handler</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is the gopherjs version:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">list</span>(<span style="color:#a6e22e">this</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Object</span>, <span style="color:#a6e22e">args</span> []<span style="color:#f92672">*</span><span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Object</span>) <span style="color:#66d9ef">interface</span>{} {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">handler</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">MakeFunc</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">this</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Object</span>, <span style="color:#a6e22e">args</span> []<span style="color:#f92672">*</span><span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Object</span>) <span style="color:#66d9ef">interface</span>{} {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">resolve</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">0</span>]
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">reject</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">args</span>[<span style="color:#ae81ff">1</span>]
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>        	<span style="color:#75715e">// (...) get maskedemails here (...)</span>
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">resolve</span>.<span style="color:#a6e22e">Invoke</span>(<span style="color:#a6e22e">out</span>)
</span></span><span style="display:flex;"><span>		}()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>	})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">promiseConstructor</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">js</span>.<span style="color:#a6e22e">Global</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;Promise&#34;</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">promiseConstructor</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">handler</span>)
</span></span></code></pre></div><p>In fact, it&rsquo;s so similar that I was able to flat-out generate the <code>gopherjs</code> main.go file with this script:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-makefile" data-lang="makefile"><span style="display:flex;"><span><span style="color:#a6e22e">.PHONY</span><span style="color:#f92672">:</span> generate-gopherjs
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">generate-gopherjs</span><span style="color:#f92672">:</span>
</span></span><span style="display:flex;"><span>	rm -rf cmd/gopherjs
</span></span><span style="display:flex;"><span>	mkdir -p cmd/gopherjs
</span></span><span style="display:flex;"><span>	cp main.go cmd/gopherjs/
</span></span><span style="display:flex;"><span>	sed -i <span style="color:#e6db74">&#39;&#39;</span> <span style="color:#e6db74">&#39;s/syscall\/js/github\.com\/gopherjs\/gopherjs\/js/g&#39;</span> cmd/gopherjs/main.go
</span></span><span style="display:flex;"><span>	sed -i <span style="color:#e6db74">&#39;&#39;</span> <span style="color:#e6db74">&#39;s/FuncOf/MakeFunc/g&#39;</span> cmd/gopherjs/main.go
</span></span><span style="display:flex;"><span>	sed -i <span style="color:#e6db74">&#39;&#39;</span> <span style="color:#e6db74">&#39;s/js\.Value/\*js\.Object/g&#39;</span> cmd/gopherjs/main.go
</span></span><span style="display:flex;"><span>	sed -i <span style="color:#e6db74">&#39;&#39;</span> <span style="color:#e6db74">&#39;s/js\.Global()/js\.Global/g&#39;</span> cmd/gopherjs/main.go
</span></span></code></pre></div><p>With a bit of JavaScript, I was able to add an automatic fallback to the gopherjs version to <code>maskedemail-js</code>, so it&rsquo;s now usable even when <code>window.WebAssembly</code> isn&rsquo;t available (although ships with 2x 8MB files).</p>
<h2 id="no-wasm-in-react-native-as-of-now">No WASM in react-native as of now</h2>
<p>A bit sad I wasn&rsquo;t able to get my goal of using real WASM working within React Native, but learned a good bit with this adventure.</p>
<p>For now, I&rsquo;ll use GopherJS when I want to re-use Go code, but I hope we&rsquo;ll see actual WebAssembly support in JSC on devices soon-ish. With all the WASM hype, I doubt it&rsquo;ll take long.</p>
<p>There is also stuff like <a href="https://wasmer.io">Wasmer</a> that can embed WASM binaries into other languages like Swift, so instead of going the JSC route, it should be possible to embed it into native modules directly or non-RN apps - for when we really want to re-use the same binary at different places. But that&rsquo;s also doable with Golang, without WASM.</p>
<p>The app is available at <a href="https://maskedemailmanager.david.coffee">https://maskedemailmanager.david.coffee</a></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Sending emails through Apple’s Hide My Email</title>
      <link>https://david.coffee/relaying-with-hide-my-email/</link>
      <pubDate>Sat, 24 Sep 2022 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/relaying-with-hide-my-email/</guid><media:content url="https://david.coffee/relaying-with-hide-my-email/hide-my-email-teaser.png" medium="image" />
          <enclosure url="https://david.coffee/relaying-with-hide-my-email/hide-my-email-teaser.png" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/relaying-with-hide-my-email/hide-my-email-teaser.png&#34; /&gt;&lt;/p&gt;&lt;p&gt;I&amp;rsquo;m a big fan of one-time-use emails (as you can probably tell by previous posts on this blog). However, one thing that iCloud&amp;rsquo;s &lt;em&gt;Hide My Email&lt;/em&gt; is still unbeatable at compared to my own domain is full anonymity. When I sign up for a junk site to get free WiFi, or order at a restaurant, I don&amp;rsquo;t even want to expose my domain to them, because that domain is, well, registered to my real identity.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Hide My Email&lt;/em&gt; on the other hand is fully anonymous: Emails come from &amp;ldquo;@icloud.com&amp;rdquo;, no name in the headers, nothing. Just a fully anonymous blank email. Great! Services can&amp;rsquo;t even filter for it (compared to mozillas solution, or duckduckgo), because filtering would mean shutting access to everything coming from icloud.com.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s one thing I wanted to get working for a while: Instead of having &lt;code&gt;&amp;lt;site or person&amp;gt; -&amp;gt; one-time-use Email (iCloud) -&amp;gt; Me&lt;/code&gt;, I want it the other way around: A one-time-use email that allows &lt;em&gt;me&lt;/em&gt; to anonymously send to a specific recipient whenever I send something to that email. Turns out you can actually do that in Apple&amp;rsquo;s Hide My Email! Let&amp;rsquo;s take a look how&lt;/p&gt;
&lt;h2 id=&#34;how-hide-my-email-actually-works&#34;&gt;How Hide My Email actually works&lt;/h2&gt;
&lt;p&gt;Hide My Email is acting as a relay in between the user account and the sender. You don&amp;rsquo;t &lt;em&gt;actually&lt;/em&gt; send emails from that randomly generated @icloud.com address. In fact, you can&amp;rsquo;t. You can&amp;rsquo;t authenticate with that email on the iCloud SMTP server because that email is not yours.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what I mean: Let&amp;rsquo;s compose a new email and use Hide My Email&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./hide-my-email-test-1.png&#34; alt=&#34;New email with Hide My Email&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;When I click &amp;ldquo;Send&amp;rdquo;, the email arrives successfully with the hidden iCloud email as sender, as expected:&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./hide-my-email-test-1-receive.png&#34; alt=&#34;How the receiver sees the email&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;&amp;hellip; but when we inspect the actually sent email, it looks like this:&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./hide-my-email-test-1-sent.png&#34; alt=&#34;How the email actually got sent&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Do you see it? The email never got sent from &amp;ldquo;&lt;a href=&#34;mailto:heave_balks_0g@icloud.com&#34;&gt;heave_balks_0g@icloud.com&lt;/a&gt;&amp;rdquo;, instead what Mail did is, it inserted a &lt;em&gt;relay address&lt;/em&gt; as the actual receiver of the email. That&amp;rsquo;s the long &lt;em&gt;test_at_davemail_io_5rvm&amp;hellip;@icloud.com&lt;/em&gt; you see there. Just that Mail is doing this so transparently that you&amp;rsquo;ll likely never even notice what is happening.&lt;/p&gt;
&lt;h3 id=&#34;relaying-back-and-forth&#34;&gt;Relaying back and forth&lt;/h3&gt;
&lt;p&gt;So now that we know this relay address, what happens when we send an email directly to it? Let&amp;rsquo;s try!&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./hide-my-email-manual-test.png&#34; alt=&#34;Manually composing a new email to the relay&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Sending, and hey look at this! Doing it manually gives us the same result! (duh)&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./hide-my-email-test-2-receive.png&#34; alt=&#34;How the email got received&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;This means that &lt;code&gt;test_at_davemail_io_5rvrmv74x77007_62a85f4e@icloud.com&lt;/code&gt; is the relay address that iCloud mapped to &lt;em&gt;test@davemail.io&lt;/em&gt; - Everything I send to it will always arrive at &lt;a href=&#34;mailto:test@davemail.io&#34;&gt;test@davemail.io&lt;/a&gt;, kind of like a mini portal that shovels content over to a specific sender.&lt;/p&gt;
&lt;p&gt;The other end will always see &lt;code&gt;heave_balks_0g@icloud.com&lt;/code&gt;, the other part of the relay, as sender. The final relay looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;heave_balks_0g@icloud.com&lt;/code&gt;: Relay &lt;em&gt;to&lt;/em&gt; my email&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test_at_davemail_io_5rvrmv74x77007_62a85f4e@icloud.com&lt;/code&gt;: Relay to the recipient&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;hide-my-email-is-unique-per-recipient-but-not-fully-random&#34;&gt;Hide My Email is unique per recipient, but not fully random&lt;/h3&gt;
&lt;p&gt;Another question you might have while following this is: Wait a moment, aren&amp;rsquo;t Hide My Email addresses random? Well, kind of.&lt;/p&gt;
&lt;p&gt;Each time you generate a hide-my-email-email (nice word), it will be random, but only per recipient: When I hide my email from &lt;a href=&#34;mailto:plans@tripit.com&#34;&gt;plans@tripit.com&lt;/a&gt;, it will &lt;em&gt;always&lt;/em&gt; be the same @icloud.com email, no matter how often I generate it. You can&amp;rsquo;t use iCloud to spam people, they&amp;rsquo;ll see everything coming from the same person.&lt;/p&gt;
&lt;h2 id=&#34;cool-but-what-can-we-actually-do-with-this-information&#34;&gt;Cool, but what can we actually do with this information??&lt;/h2&gt;
&lt;p&gt;Obviously this post was leading up to something, otherwise I would have not put so much time into writing it.&lt;/p&gt;
&lt;p&gt;Now that we know that relay addresses don&amp;rsquo;t change, we can use this knowledge to automate sending completely anonymous emails to specific senders without exposing our main domain, name or any other information.&lt;/p&gt;
&lt;p&gt;Even better: Using the relay even shields us from any mistakes, like accidentally leaking personal information in headers. It happened dozen times that I used &amp;ldquo;forward email&amp;rdquo; functionality in email clients to forward an email to a service, just to realize that the email service sent that email with my global account email, instead of the one-time-use email. (How would the service even know what email I want to use for forwarding?)&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s take a service like &lt;a href=&#34;https://www.tripit.com/web&#34;&gt;TripIt&lt;/a&gt; which gives me an email that I can send flight itineraries to: &lt;a href=&#34;mailto:plans@tripit.com&#34;&gt;plans@tripit.com&lt;/a&gt;. I can link an email to it like &amp;ldquo;&lt;a href=&#34;mailto:tripit.com@mydomain.com&#34;&gt;tripit.com@mydomain.com&lt;/a&gt;&amp;rdquo;, then whenever I send an email &lt;em&gt;from&lt;/em&gt; this email to &lt;a href=&#34;mailto:plans@tripit.com&#34;&gt;plans@tripit.com&lt;/a&gt;, it will add that itinerary to my account. This works great, but means I can&amp;rsquo;t really setup any quick-actions, shortcuts or automations to quickly forward to it, because of the issue I described above. Now we can fix that by directly using the relay address :)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Send an email to &lt;a href=&#34;mailto:plans@tripit.com&#34;&gt;plans@tripit.com&lt;/a&gt; using Hide My Email&lt;/li&gt;
&lt;li&gt;Copy the generated @icloud.com address and add it to TripIt&lt;/li&gt;
&lt;li&gt;Copy the generated relay address (the long one aka &lt;code&gt;test_at_davemail_io_5rvrmv74x77007_62a85f4e@icloud.com&lt;/code&gt;), create a new contact with it for quick access&lt;/li&gt;
&lt;li&gt;Profit.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now we can hit &amp;ldquo;forward&amp;rdquo; (or use a Siri Shortcut) to forward any PDF attachment we want to add to tripit to our relay address, iCloud will do the rest.&lt;/p&gt;
&lt;p&gt;All of this effort just so we don&amp;rsquo;t expose our main email, huh 😉&lt;/p&gt;
&lt;p&gt;Keep on shaving that yak&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./hide-my-email-yak.png&#34; alt=&#34;The Yak&#34; /&gt;

&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/relaying-with-hide-my-email/hide-my-email-teaser.png" /></p><p>I&rsquo;m a big fan of one-time-use emails (as you can probably tell by previous posts on this blog). However, one thing that iCloud&rsquo;s <em>Hide My Email</em> is still unbeatable at compared to my own domain is full anonymity. When I sign up for a junk site to get free WiFi, or order at a restaurant, I don&rsquo;t even want to expose my domain to them, because that domain is, well, registered to my real identity.</p>
<p><em>Hide My Email</em> on the other hand is fully anonymous: Emails come from &ldquo;@icloud.com&rdquo;, no name in the headers, nothing. Just a fully anonymous blank email. Great! Services can&rsquo;t even filter for it (compared to mozillas solution, or duckduckgo), because filtering would mean shutting access to everything coming from icloud.com.</p>
<p>Here&rsquo;s one thing I wanted to get working for a while: Instead of having <code>&lt;site or person&gt; -&gt; one-time-use Email (iCloud) -&gt; Me</code>, I want it the other way around: A one-time-use email that allows <em>me</em> to anonymously send to a specific recipient whenever I send something to that email. Turns out you can actually do that in Apple&rsquo;s Hide My Email! Let&rsquo;s take a look how</p>
<h2 id="how-hide-my-email-actually-works">How Hide My Email actually works</h2>
<p>Hide My Email is acting as a relay in between the user account and the sender. You don&rsquo;t <em>actually</em> send emails from that randomly generated @icloud.com address. In fact, you can&rsquo;t. You can&rsquo;t authenticate with that email on the iCloud SMTP server because that email is not yours.</p>
<p>Here&rsquo;s what I mean: Let&rsquo;s compose a new email and use Hide My Email</p>
<p>
<img class="img-fluid" src="./hide-my-email-test-1.png" alt="New email with Hide My Email" />

</p>
<p>When I click &ldquo;Send&rdquo;, the email arrives successfully with the hidden iCloud email as sender, as expected:</p>
<p>
<img class="img-fluid" src="./hide-my-email-test-1-receive.png" alt="How the receiver sees the email" />

</p>
<p>&hellip; but when we inspect the actually sent email, it looks like this:</p>
<p>
<img class="img-fluid" src="./hide-my-email-test-1-sent.png" alt="How the email actually got sent" />

</p>
<p>Do you see it? The email never got sent from &ldquo;<a href="mailto:heave_balks_0g@icloud.com">heave_balks_0g@icloud.com</a>&rdquo;, instead what Mail did is, it inserted a <em>relay address</em> as the actual receiver of the email. That&rsquo;s the long <em>test_at_davemail_io_5rvm&hellip;@icloud.com</em> you see there. Just that Mail is doing this so transparently that you&rsquo;ll likely never even notice what is happening.</p>
<h3 id="relaying-back-and-forth">Relaying back and forth</h3>
<p>So now that we know this relay address, what happens when we send an email directly to it? Let&rsquo;s try!</p>
<p>
<img class="img-fluid" src="./hide-my-email-manual-test.png" alt="Manually composing a new email to the relay" />

</p>
<p>Sending, and hey look at this! Doing it manually gives us the same result! (duh)</p>
<p>
<img class="img-fluid" src="./hide-my-email-test-2-receive.png" alt="How the email got received" />

</p>
<p>This means that <code>test_at_davemail_io_5rvrmv74x77007_62a85f4e@icloud.com</code> is the relay address that iCloud mapped to <em>test@davemail.io</em> - Everything I send to it will always arrive at <a href="mailto:test@davemail.io">test@davemail.io</a>, kind of like a mini portal that shovels content over to a specific sender.</p>
<p>The other end will always see <code>heave_balks_0g@icloud.com</code>, the other part of the relay, as sender. The final relay looks like this:</p>
<ul>
<li><code>heave_balks_0g@icloud.com</code>: Relay <em>to</em> my email</li>
<li><code>test_at_davemail_io_5rvrmv74x77007_62a85f4e@icloud.com</code>: Relay to the recipient</li>
</ul>
<h3 id="hide-my-email-is-unique-per-recipient-but-not-fully-random">Hide My Email is unique per recipient, but not fully random</h3>
<p>Another question you might have while following this is: Wait a moment, aren&rsquo;t Hide My Email addresses random? Well, kind of.</p>
<p>Each time you generate a hide-my-email-email (nice word), it will be random, but only per recipient: When I hide my email from <a href="mailto:plans@tripit.com">plans@tripit.com</a>, it will <em>always</em> be the same @icloud.com email, no matter how often I generate it. You can&rsquo;t use iCloud to spam people, they&rsquo;ll see everything coming from the same person.</p>
<h2 id="cool-but-what-can-we-actually-do-with-this-information">Cool, but what can we actually do with this information??</h2>
<p>Obviously this post was leading up to something, otherwise I would have not put so much time into writing it.</p>
<p>Now that we know that relay addresses don&rsquo;t change, we can use this knowledge to automate sending completely anonymous emails to specific senders without exposing our main domain, name or any other information.</p>
<p>Even better: Using the relay even shields us from any mistakes, like accidentally leaking personal information in headers. It happened dozen times that I used &ldquo;forward email&rdquo; functionality in email clients to forward an email to a service, just to realize that the email service sent that email with my global account email, instead of the one-time-use email. (How would the service even know what email I want to use for forwarding?)</p>
<p>Let&rsquo;s take a service like <a href="https://www.tripit.com/web">TripIt</a> which gives me an email that I can send flight itineraries to: <a href="mailto:plans@tripit.com">plans@tripit.com</a>. I can link an email to it like &ldquo;<a href="mailto:tripit.com@mydomain.com">tripit.com@mydomain.com</a>&rdquo;, then whenever I send an email <em>from</em> this email to <a href="mailto:plans@tripit.com">plans@tripit.com</a>, it will add that itinerary to my account. This works great, but means I can&rsquo;t really setup any quick-actions, shortcuts or automations to quickly forward to it, because of the issue I described above. Now we can fix that by directly using the relay address :)</p>
<ol>
<li>Send an email to <a href="mailto:plans@tripit.com">plans@tripit.com</a> using Hide My Email</li>
<li>Copy the generated @icloud.com address and add it to TripIt</li>
<li>Copy the generated relay address (the long one aka <code>test_at_davemail_io_5rvrmv74x77007_62a85f4e@icloud.com</code>), create a new contact with it for quick access</li>
<li>Profit.</li>
</ol>
<p>Now we can hit &ldquo;forward&rdquo; (or use a Siri Shortcut) to forward any PDF attachment we want to add to tripit to our relay address, iCloud will do the rest.</p>
<p>All of this effort just so we don&rsquo;t expose our main email, huh 😉</p>
<p>Keep on shaving that yak</p>
<p>
<img class="img-fluid" src="./hide-my-email-yak.png" alt="The Yak" />

</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Teaching my UniFi UDM how to IPv6</title>
      <link>https://david.coffee/teaching-my-unifi-udm-how-to-ipv6/</link>
      <pubDate>Wed, 18 May 2022 04:50:23 +0000</pubDate>
      
      <guid>https://david.coffee/teaching-my-unifi-udm-how-to-ipv6/</guid><media:content url="https://david.coffee/teaching-my-unifi-udm-how-to-ipv6/ipv6-test.png" medium="image" />
          <enclosure url="https://david.coffee/teaching-my-unifi-udm-how-to-ipv6/ipv6-test.png" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/teaching-my-unifi-udm-how-to-ipv6/ipv6-test.png&#34; /&gt;&lt;/p&gt;&lt;p&gt;I recently ventured into IPv6 territory with one goal: Get rid of the router my ISP gave me.&lt;/p&gt;
&lt;p&gt;I am a fan of UniFi gear and own an &lt;em&gt;UniFi Dream Machine&lt;/em&gt; (upgraded from a UniFi Secure Gateway last year) which is more than capable of handling IPv6, so how hard can it be?&lt;/p&gt;
&lt;p&gt;I went into this not knowing a thing about IPv6 and just assumed it was identical to IPv4, just bigger, &lt;strong&gt;but boy was I wrong.&lt;/strong&gt; This is my writeup of what I learned and what my endresult is (spoiler: I got it working)&lt;/p&gt;
&lt;h2 id=&#34;ipv6-misconceptions&#34;&gt;IPv6 misconceptions&lt;/h2&gt;
&lt;p&gt;As I explained above, I just assumed that IPv6 was IPv4, but longer. So you have some gateway/subnet-mask combo, your router gets an IP assigned from the ISP and all devices inside the network use that IP outwards facing when interacting with things (= &amp;ldquo;NAT&amp;rdquo;). Internally, the router assigns IPv4 addresses based on the gateway/subnet-mask, with each device receiving a unique IPv4 address from the router through DHCP. So far so good.&lt;/p&gt;
&lt;p&gt;IPv6 on the other hand does not work at all like this. In IPv6-land, each &lt;strong&gt;device&lt;/strong&gt; receives unique (actually multiple) IPv6 addresses. There is no NAT, so when my iPhone that&amp;rsquo;s connected to my WiFi communicates with a website, that website will see the unique IPv6 address my iPhone has assigned, not my router&amp;rsquo;s IP. There are also (unless specifically setup) no local network addresses - each IPv6 address is global. And how can the router assign IPv6 addresses? Enter prefix delegation or IPv6-PD.&lt;/p&gt;
&lt;p&gt;The router, after authenticating with the ISP when PD is enabled, receives an entire address space (like /64 or /48) delegated to it. In simple words, the ISP tells the router: &amp;ldquo;Here&amp;rsquo;s a chunk of addresses that you can use for your devices, handle it.&amp;rdquo;&lt;/p&gt;
&lt;h3 id=&#34;not-all-ipv6-addresses-are-created-equal&#34;&gt;Not all IPv6 addresses are created equal&lt;/h3&gt;
&lt;p&gt;Let&amp;rsquo;s take a look at this ifconfig output of my Mac:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;en0: flags=8863&amp;lt;UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST&amp;gt; mtu 1500
        options=50b&amp;lt;RXCSUM,TXCSUM,VLAN_HWTAGGING,AV,CHANNEL_IO&amp;gt;
        ether 4c:20:b8:e7:3b:be
        inet6 fe80::1449:6fa5:65a9:a313%en0 prefixlen 64 secured scopeid 0x6
        inet6 fd74:d39f:a55f::8c prefixlen 64 dynamic
        inet 192.168.1.2 netmask 0xffffff00 broadcast 192.168.1.255
        inet6 240f:78:&amp;lt;snip&amp;gt; prefixlen 64 dynamic
        inet6 240f:78:&amp;lt;snip&amp;gt; prefixlen 64 autoconf secured
        inet6 240f:78:&amp;lt;snip&amp;gt; prefixlen 64 autoconf temporary
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It doesn&amp;rsquo;t just have 1 IPv6 address, no that would be far too simple. Instead, it has 5!! Why? Because apparently, we have so many IPv6 addresses now that we don&amp;rsquo;t need to be sparse with them /s.&lt;/p&gt;
&lt;p&gt;There are different kinds of IPv6 addresses assigned to my mac:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;inet6 240f:78:&amp;lt;snip&amp;gt; prefixlen 64 dynamic
inet6 240f:78:&amp;lt;snip&amp;gt; prefixlen 64 autoconf secured
inet6 240f:78:&amp;lt;snip&amp;gt; prefixlen 64 autoconf temporary
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;secured&lt;/code&gt; address is my permanent IPv6 address. This address rarely ever changes and will stick around even after reboots.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;temporary&lt;/code&gt; address is, you guessed it, a temporary address.&lt;/p&gt;
&lt;p&gt;Lastly we have 2 more:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;inet6 fe80::1449:6fa5:65a9:a313%en0 prefixlen 64 secured scopeid 0x6
inet6 fd74:d39f:a55f::8c prefixlen 64 dynamic
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;fe80::&lt;/code&gt;, while not obvious whatsoever, is a prefix for &lt;code&gt;localhost&lt;/code&gt; or local link. This is an address that&amp;rsquo;s only usable locally.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fd74::&lt;/code&gt;, more specifically the &lt;code&gt;fd::&lt;/code&gt; prefix is designated for the local network. Didn&amp;rsquo;t I just earlier say that there are no local network addresses in IPv6? By default, yes, but I specifically setup my router to hand out local network addresses. Keyword here is &amp;ldquo;&lt;a href=&#34;https://en.wikipedia.org/wiki/Unique_local_address&#34;&gt;Unique local address&lt;/a&gt;&amp;rdquo; (ULA).&lt;/p&gt;
&lt;h2 id=&#34;ipv6-implementations---native-6to4-ipv6oe&#34;&gt;IPv6 implementations - Native, 6to4, IPv6oE&lt;/h2&gt;
&lt;p&gt;Are you still with me?? Okay, because there is more. Not all IPv6 implementations are equal either. Very common implementations are either IPv6 &amp;ldquo;native&amp;rdquo; or &amp;ldquo;IPv6-in-IPv4 tunnel&amp;rdquo;. There are more exotic implementations like SoftBanks &amp;ldquo;IPv6高速ハイブリッド IPv6 IPoE + IPv4&amp;rdquo; that&amp;rsquo;s utilizing IPoE for authentication and assigning addresses. (&lt;em&gt;Note here: I used SoftBank in the past and was not able to get IPv6 without their router working due to the proprietary way their router authenticates&lt;/em&gt;)&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not going into detail here and don&amp;rsquo;t fully understand everything either, but a 6to4 tunnel is like the name implies an earlier implementation that tries to build the IPv6 stack on top of a IPv4 network stack. Read more about it &lt;a href=&#34;https://support.huawei.com/enterprise/en/doc/EDOC1100055124/58a0a1e4/understanding-ipv6-over-ipv4-tunnel-technology&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&amp;ldquo;Native&amp;rdquo; IPv6 is the one that&amp;rsquo;s nice to work with, but also the one not every ISP supports. I made sure to pick an ISP that has a native IPv6 stack and doesn&amp;rsquo;t use IPv6-over-IPv4&lt;/p&gt;
&lt;h2 id=&#34;getting-started---figuring-out-what-my-isp-router-is-doing&#34;&gt;Getting started - figuring out what my ISP router is doing&lt;/h2&gt;
&lt;p&gt;The first step to getting IPv6 working was to actually figure out what my ISP router is doing when I plug it in. Specifically: How does it actually get the IPv6 prefix assigned, how does it authenticate, and what does the handhshake look like.&lt;/p&gt;
&lt;p&gt;Now how do we actually do this? &lt;a href=&#34;https://www.wireshark.org&#34;&gt;Wireshark&lt;/a&gt; and a couple of USB-C network dongles to the rescue!&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./dongles.jpg&#34; alt=&#34;dongles&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Mac has a lot of nice networking features, one of them is the ability to bridge 2 network interfaces, effectively creating a tunnel between them. This can be done through the CLI, but also through the network preferences pane after plugging multiple network dongles into the Mac.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./network-preference-pane.png&#34; alt=&#34;network-preference-pane&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Specifically, we want to make it so the MacBook is &lt;strong&gt;in between&lt;/strong&gt; the ISP Router and the network out and acting as a transparent bridge. So &lt;strong&gt;Internet &amp;lt;-&amp;gt; MacBook &amp;lt;-&amp;gt; ISP Router&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Once the virtual bridge is created, we can use Wireshark to monitor the new device for any traffic that happens between the ISP router and the mothership. Now start Wireshark, plug the ISP router in, and&amp;hellip;.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./au-hikari-startup.png&#34; alt=&#34;au-hikari-startup&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;&amp;hellip; it works!&lt;/p&gt;
&lt;p&gt;Lot&amp;rsquo;s of new information here for a non-network engineer like me, but also some familiar words: DHCPv6, ICMPv6, UDP, SSL. The &lt;code&gt;*v6&lt;/code&gt; protocols are IPv6 variants of the same things we have in IPv4, but beefed up and more important in IPv6 land.&lt;/p&gt;
&lt;p&gt;Specifically, &lt;code&gt;ICMPv6&lt;/code&gt; is much more important than the IPv4 version that we use for pinging stuff and is used extensively for the &lt;em&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Neighbor_Discovery_Protocol&#34;&gt;Neighbor Discovery Protocol&lt;/a&gt;&lt;/em&gt;, that&amp;rsquo;s those &amp;ldquo;Router Solicitation&amp;rdquo; and &amp;ldquo;Router Advertisement&amp;rdquo; messages you see in the screenshot above.&lt;/p&gt;
&lt;p&gt;So from this startup dump we can see:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;My ISP router inquires with ISP uplink through &lt;em&gt;Router Solicitation&lt;/em&gt; where the uplink router is&lt;/li&gt;
&lt;li&gt;Uplink responds with &lt;em&gt;Router Advertisement&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;ISP router advertises itself through DHCPv6, uses a DUID (&amp;ldquo;DHCP unique identifier&amp;rdquo;) to identify itself, this DUID is generated off the mac address&lt;/li&gt;
&lt;li&gt;ISP responds to DHCPv6 request and assigns IPv6 prefix&lt;/li&gt;
&lt;li&gt;Neighbor Discovery Protocol continues with Neighbor Solicitation and Neighbor Advertisement&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(the XID stuff you see in there is a transaction id that&amp;rsquo;s being calculated on each request)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;So to summarize&lt;/strong&gt;: This ISP is using fully native IPv6, the IPv6 prefix space is assigned through DHCPv6, and authentication happens through a DUID identifier that&amp;rsquo;s being used in all DHCPv6 requests.&lt;/p&gt;
&lt;p&gt;If we are able to replicate this handshake on the UDM, we should have IPv6 working. Now how do we actually do that&amp;hellip;&lt;/p&gt;
&lt;h2 id=&#34;teaching-the-udm-how-to-dhcpv6&#34;&gt;Teaching the UDM how to DHCPv6&lt;/h2&gt;
&lt;p&gt;From the wireshark dump above, we know that the ISP router is using DHCPv6 to authenticate and get its prefix assigned. We also know which DUID the ISP router uses for identifying itself, and we know the prefix size that we get assigned from the ISP (/48).&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s enable IPv6 through DHCPv6 through the UniFi admin on WAN:&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./ipv6-settings.png&#34; alt=&#34;ipv6-settings&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Then grep for &lt;code&gt;dhcp&lt;/code&gt; to see what&amp;rsquo;s happening on the UDM:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;# ps aux | grep dhcp
 3537 root     /sbin/udhcpc --foreground --interface eth4 --script /usr/share/ubios-udapi-server/ubios-udhcpc-script -r 124.214.62.19
 3895 root     /usr/sbin/odhcp6c -R -f -e -v -s /usr/share/ubios-udapi-server/ubios-odhcp6c-script -P 48 eth4
 7715 root     grep dhcp
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The UDM is spinning up &lt;code&gt;udhcpc&lt;/code&gt; and &lt;code&gt;odhcp6c&lt;/code&gt; with the closed-source binaries &lt;code&gt;ubios-odhcp6c-script&lt;/code&gt;, both on the &lt;code&gt;eth4&lt;/code&gt; interface (my WAN port).&lt;/p&gt;
&lt;p&gt;We can also print our own &lt;code&gt;duid&lt;/code&gt; by running &lt;code&gt;cat /config/dnsmasq.lease&lt;/code&gt;, and since the DUID being used is not the one that our ISP router uses, we have to do something about it.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/openwrt/odhcp6c&#34;&gt;odhcp6c&lt;/a&gt; that is being used here is is a DHCPv6 client by openwrt and it being open-source and widely used means we can dig deeper into what it&amp;rsquo;s doing with all those parameters:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Usage: odhcp6c [options] &amp;lt;interface&amp;gt;

Feature options:
        -S &amp;lt;time&amp;gt;       Wait at least &amp;lt;time&amp;gt; sec for a DHCP-server (0)
        -N &amp;lt;mode&amp;gt;       Mode for requesting addresses [try|force|none]
        -P &amp;lt;length&amp;gt;     Request IPv6-Prefix (0 = auto)
        -F              Force IPv6-Prefix
        -V &amp;lt;class&amp;gt;      Set vendor-class option (base-16 encoded)
        -u &amp;lt;user-class&amp;gt; Set user-class option string
        -c &amp;lt;clientid&amp;gt;   Override client-ID (base-16 encoded 16-bit type + value)
        -i &amp;lt;iface-id&amp;gt;   Use a custom interface identifier for RA handling
        -r &amp;lt;options&amp;gt;    Options to be requested (comma-separated)
        -R              Do not request any options except those specified with -r
        -s &amp;lt;script&amp;gt;     Status update script (/usr/sbin/odhcp6c-update)
        -a              Don&amp;#39;t send Accept Reconfigure option
        -f              Don&amp;#39;t send Client FQDN option
        -k              Don&amp;#39;t send a RELEASE when stopping
        -t &amp;lt;seconds&amp;gt;    Maximum timeout for DHCPv6-SOLICIT (120)
        -m &amp;lt;seconds&amp;gt;    Minimum time between accepting updates (30)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The flag we&amp;rsquo;re after is &lt;code&gt;-c &amp;lt;clientid&amp;gt;   Override client-ID (base-16 encoded 16-bit type + value) &lt;/code&gt;. So theoretically, if we are able to pass &lt;code&gt;-c&lt;/code&gt; into the startup of the &lt;code&gt;odhcp6c&lt;/code&gt; command, we should be able to receive a valid IPv6 address from the ISP. Let&amp;rsquo;s give it a try!&lt;/p&gt;
&lt;p&gt;Disable DHCPv6 again through the admin UI, wait until the process fully stopped, then manually run the command above, but with &lt;code&gt;-c&lt;/code&gt; flag:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;/usr/sbin/odhcp6c -R -f -c &amp;lt;DUID from our wireshark dump&amp;gt;  -e -v -s /usr/share/ubios-udapi-server/ubios-odhcp6c-script -P 48 eth4
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Wait a bit, then &lt;code&gt;ip -6&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;# ip -6 addr show scope global
10: br0: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 state UP qlen 1000
    inet6 fd74:d39f:a55f::1/64 scope global
       valid_lft forever preferred_lft forever
    inet6 240f:78:&amp;lt;snip&amp;gt;/64 scope global dynamic
       valid_lft 108367sec preferred_lft 108367sec
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hello there, IPv6 :)&lt;/p&gt;
&lt;p&gt;Back on the UDM admin, let&amp;rsquo;s enable IPv6 on the LAN network. We want to do prefix delegation and also advertise ourselves as a router (remember &lt;em&gt;Neighbor Discovery Protocol&lt;/em&gt; above?)&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./ipv6-lan-settings.png&#34; alt=&#34;ipv6-lan-settings&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Wait a bit, then let&amp;rsquo;s enable IPv6 on my mac and renew the DHCPv6 lease:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;en0: flags=8863&amp;lt;UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST&amp;gt; mtu 1500
        inet6 240f:78:9464::&amp;lt;snip&amp;gt; prefixlen 64 dynamic
        inet6 240f:78:9464:0:&amp;lt;snip&amp;gt; prefixlen 64 autoconf secured
        inet6 240f:78:9464:0:&amp;lt;snip&amp;gt; prefixlen 64 autoconf temporary
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Nice!!&lt;/p&gt;
&lt;p&gt;And what about &lt;a href=&#34;https://test-ipv6.com&#34;&gt;https://test-ipv6.com&lt;/a&gt;?&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./ipv6-test.png&#34; alt=&#34;ipv6-test&#34; /&gt;

&lt;/p&gt;
&lt;h3 id=&#34;persisting-changes-to-the-udm&#34;&gt;Persisting changes to the UDM&lt;/h3&gt;
&lt;p&gt;Now that we got an IPv6 address, let&amp;rsquo;s persist those changes so we don&amp;rsquo;t have to manually run odhcpv6 every time.&lt;/p&gt;
&lt;h4 id=&#34;pre-req-extending-the-udm-with-boot-scripts&#34;&gt;Pre-req: Extending the UDM with boot-scripts&lt;/h4&gt;
&lt;p&gt;UniFi gear is &lt;em&gt;hackable&lt;/em&gt; which allows us to do more than just what&amp;rsquo;s possible through the admin UI. The USG had a very advanced customization flow baked in, but the UDM is a tad more annoying to work with.&lt;/p&gt;
&lt;p&gt;Luckily there is this awesome project called &lt;a href=&#34;https://github.com/boostchicken-dev/udm-utilities&#34;&gt;udm-utilities&lt;/a&gt; by boostchicken, a &amp;ldquo;&lt;em&gt;A collection of things to enhance the capabilities of your Unifi Dream Machine or Dream Machine Pro.&lt;/em&gt;&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Basically what this project does is, it installs a custom package that extends the UDM with the ability to run scripts on boot that persist in between reboots. We put our shell scripts into &lt;code&gt;/mnt/data/on_boot.d&lt;/code&gt;, and udm-utilities makes sure those are run when the router boots.&lt;/p&gt;
&lt;p&gt;There are a lot of really cool things you can do with it, from configuring the UDM to even running podman containers like homebridge or adguard, but we&amp;rsquo;ll be focusing on the network aspects only for this writeup.&lt;/p&gt;
&lt;h4 id=&#34;changing-duid-on-boot&#34;&gt;Changing DUID on boot&lt;/h4&gt;
&lt;p&gt;Now the next problem is, how do we actually start &lt;code&gt;odhcp6c&lt;/code&gt; with our own options? The UDM launches it on boot through &lt;code&gt;ubios-udapi-server&lt;/code&gt;, which is closed-source. We could decompile the binary with ghidra and see if we can hack our own arguments in, how hard can it be?&lt;/p&gt;
&lt;p&gt;But there is an easier (more hacky) way of achieving just that. What if the binary at &lt;code&gt;/usr/sbin/odhcp6c&lt;/code&gt; isn&amp;rsquo;t actually a binary, but an executable script that runs &lt;code&gt;odhcp6c&lt;/code&gt; for us? The UDM will continue to execute &lt;code&gt;odhcp6c&lt;/code&gt; like usual and we can inject our custom flags. Here&amp;rsquo;s what I mean:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# cat /usr/sbin/odhcp6c&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#!/bin/sh&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# workaround script to inject odhcp6 options&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/usr/sbin/odhcp6c-real -R -f -c 0003000&amp;lt;snip&amp;gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$@&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The custom script that we put in &lt;code&gt;/usr/sbin&lt;/code&gt; is calling &lt;code&gt;odhcp6c&lt;/code&gt; transparently and adds the extra &lt;code&gt;-c&lt;/code&gt; flag.&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;udm-utilities&lt;/code&gt;, we can automate this with the following boot script:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;#!/bin/sh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;mv /usr/sbin/odhcp6c /usr/sbin/odhcp6c-real
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cp /mnt/data/odhcp6c/odhcp6c /usr/sbin/odhcp6c
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;chmod +x /usr/sbin/odhcp6c
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;killall odhcp6c
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol&gt;
&lt;li&gt;We move &lt;code&gt;odhcp6c&lt;/code&gt; to &lt;code&gt;odhcp6c-real&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Copy the custom &lt;code&gt;odhcp6c&lt;/code&gt; to the old location&lt;/li&gt;
&lt;li&gt;Make it executable&lt;/li&gt;
&lt;li&gt;Kill odhcp6c&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once killed, &lt;code&gt;ubios-udapi-server&lt;/code&gt; will automatically attempt to restart it by executing &lt;code&gt;/usr/sbin/odhcp6c&lt;/code&gt;, which is no longer the real binary but our custom script.&lt;/p&gt;
&lt;p&gt;And does it work?&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;# ps aux | grep dhcp
 3891 root     {odhcp6c} /bin/sh /usr/sbin/odhcp6c -e -v -s /usr/share/ubios-udapi-server/ubios-odhcp6c-script -P 64 eth4
 3895 root     /usr/sbin/odhcp6c-real -R -f -c 0003000&amp;lt;snip&amp;gt; -e -v -s /usr/share/ubios-udapi-server/ubios-odhcp6c-script -P 64 eth4
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It absolutely does!&lt;/p&gt;
&lt;h2 id=&#34;was-it-worth-it&#34;&gt;Was it worth it?&lt;/h2&gt;
&lt;p&gt;Worth it in terms of performance or network benefit? Probably not. The world doesn&amp;rsquo;t feel different when interacting with IPv6 and speed for me is pretty much on par as it was with IPv4.&lt;/p&gt;
&lt;p&gt;But it has been a very nice learning experience. Without this venture into IPv6-land I would have never learned about the differences between IPv6 and IPv4.&lt;/p&gt;
&lt;p&gt;My network is now fully IPv6 compatible which it wasn&amp;rsquo;t before, and that&amp;rsquo;s an achievement in itself.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/teaching-my-unifi-udm-how-to-ipv6/ipv6-test.png" /></p><p>I recently ventured into IPv6 territory with one goal: Get rid of the router my ISP gave me.</p>
<p>I am a fan of UniFi gear and own an <em>UniFi Dream Machine</em> (upgraded from a UniFi Secure Gateway last year) which is more than capable of handling IPv6, so how hard can it be?</p>
<p>I went into this not knowing a thing about IPv6 and just assumed it was identical to IPv4, just bigger, <strong>but boy was I wrong.</strong> This is my writeup of what I learned and what my endresult is (spoiler: I got it working)</p>
<h2 id="ipv6-misconceptions">IPv6 misconceptions</h2>
<p>As I explained above, I just assumed that IPv6 was IPv4, but longer. So you have some gateway/subnet-mask combo, your router gets an IP assigned from the ISP and all devices inside the network use that IP outwards facing when interacting with things (= &ldquo;NAT&rdquo;). Internally, the router assigns IPv4 addresses based on the gateway/subnet-mask, with each device receiving a unique IPv4 address from the router through DHCP. So far so good.</p>
<p>IPv6 on the other hand does not work at all like this. In IPv6-land, each <strong>device</strong> receives unique (actually multiple) IPv6 addresses. There is no NAT, so when my iPhone that&rsquo;s connected to my WiFi communicates with a website, that website will see the unique IPv6 address my iPhone has assigned, not my router&rsquo;s IP. There are also (unless specifically setup) no local network addresses - each IPv6 address is global. And how can the router assign IPv6 addresses? Enter prefix delegation or IPv6-PD.</p>
<p>The router, after authenticating with the ISP when PD is enabled, receives an entire address space (like /64 or /48) delegated to it. In simple words, the ISP tells the router: &ldquo;Here&rsquo;s a chunk of addresses that you can use for your devices, handle it.&rdquo;</p>
<h3 id="not-all-ipv6-addresses-are-created-equal">Not all IPv6 addresses are created equal</h3>
<p>Let&rsquo;s take a look at this ifconfig output of my Mac:</p>
<pre tabindex="0"><code>en0: flags=8863&lt;UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST&gt; mtu 1500
        options=50b&lt;RXCSUM,TXCSUM,VLAN_HWTAGGING,AV,CHANNEL_IO&gt;
        ether 4c:20:b8:e7:3b:be
        inet6 fe80::1449:6fa5:65a9:a313%en0 prefixlen 64 secured scopeid 0x6
        inet6 fd74:d39f:a55f::8c prefixlen 64 dynamic
        inet 192.168.1.2 netmask 0xffffff00 broadcast 192.168.1.255
        inet6 240f:78:&lt;snip&gt; prefixlen 64 dynamic
        inet6 240f:78:&lt;snip&gt; prefixlen 64 autoconf secured
        inet6 240f:78:&lt;snip&gt; prefixlen 64 autoconf temporary
</code></pre><p>It doesn&rsquo;t just have 1 IPv6 address, no that would be far too simple. Instead, it has 5!! Why? Because apparently, we have so many IPv6 addresses now that we don&rsquo;t need to be sparse with them /s.</p>
<p>There are different kinds of IPv6 addresses assigned to my mac:</p>
<pre tabindex="0"><code>inet6 240f:78:&lt;snip&gt; prefixlen 64 dynamic
inet6 240f:78:&lt;snip&gt; prefixlen 64 autoconf secured
inet6 240f:78:&lt;snip&gt; prefixlen 64 autoconf temporary
</code></pre><p>The <code>secured</code> address is my permanent IPv6 address. This address rarely ever changes and will stick around even after reboots.</p>
<p>The <code>temporary</code> address is, you guessed it, a temporary address.</p>
<p>Lastly we have 2 more:</p>
<pre tabindex="0"><code>inet6 fe80::1449:6fa5:65a9:a313%en0 prefixlen 64 secured scopeid 0x6
inet6 fd74:d39f:a55f::8c prefixlen 64 dynamic
</code></pre><p><code>fe80::</code>, while not obvious whatsoever, is a prefix for <code>localhost</code> or local link. This is an address that&rsquo;s only usable locally.</p>
<p><code>fd74::</code>, more specifically the <code>fd::</code> prefix is designated for the local network. Didn&rsquo;t I just earlier say that there are no local network addresses in IPv6? By default, yes, but I specifically setup my router to hand out local network addresses. Keyword here is &ldquo;<a href="https://en.wikipedia.org/wiki/Unique_local_address">Unique local address</a>&rdquo; (ULA).</p>
<h2 id="ipv6-implementations---native-6to4-ipv6oe">IPv6 implementations - Native, 6to4, IPv6oE</h2>
<p>Are you still with me?? Okay, because there is more. Not all IPv6 implementations are equal either. Very common implementations are either IPv6 &ldquo;native&rdquo; or &ldquo;IPv6-in-IPv4 tunnel&rdquo;. There are more exotic implementations like SoftBanks &ldquo;IPv6高速ハイブリッド IPv6 IPoE + IPv4&rdquo; that&rsquo;s utilizing IPoE for authentication and assigning addresses. (<em>Note here: I used SoftBank in the past and was not able to get IPv6 without their router working due to the proprietary way their router authenticates</em>)</p>
<p>I&rsquo;m not going into detail here and don&rsquo;t fully understand everything either, but a 6to4 tunnel is like the name implies an earlier implementation that tries to build the IPv6 stack on top of a IPv4 network stack. Read more about it <a href="https://support.huawei.com/enterprise/en/doc/EDOC1100055124/58a0a1e4/understanding-ipv6-over-ipv4-tunnel-technology">here</a>.</p>
<p>&ldquo;Native&rdquo; IPv6 is the one that&rsquo;s nice to work with, but also the one not every ISP supports. I made sure to pick an ISP that has a native IPv6 stack and doesn&rsquo;t use IPv6-over-IPv4</p>
<h2 id="getting-started---figuring-out-what-my-isp-router-is-doing">Getting started - figuring out what my ISP router is doing</h2>
<p>The first step to getting IPv6 working was to actually figure out what my ISP router is doing when I plug it in. Specifically: How does it actually get the IPv6 prefix assigned, how does it authenticate, and what does the handhshake look like.</p>
<p>Now how do we actually do this? <a href="https://www.wireshark.org">Wireshark</a> and a couple of USB-C network dongles to the rescue!</p>
<p>
<img class="img-fluid" src="./dongles.jpg" alt="dongles" />

</p>
<p>Mac has a lot of nice networking features, one of them is the ability to bridge 2 network interfaces, effectively creating a tunnel between them. This can be done through the CLI, but also through the network preferences pane after plugging multiple network dongles into the Mac.</p>
<p>
<img class="img-fluid" src="./network-preference-pane.png" alt="network-preference-pane" />

</p>
<p>Specifically, we want to make it so the MacBook is <strong>in between</strong> the ISP Router and the network out and acting as a transparent bridge. So <strong>Internet &lt;-&gt; MacBook &lt;-&gt; ISP Router</strong></p>
<p>Once the virtual bridge is created, we can use Wireshark to monitor the new device for any traffic that happens between the ISP router and the mothership. Now start Wireshark, plug the ISP router in, and&hellip;.</p>
<p>
<img class="img-fluid" src="./au-hikari-startup.png" alt="au-hikari-startup" />

</p>
<p>&hellip; it works!</p>
<p>Lot&rsquo;s of new information here for a non-network engineer like me, but also some familiar words: DHCPv6, ICMPv6, UDP, SSL. The <code>*v6</code> protocols are IPv6 variants of the same things we have in IPv4, but beefed up and more important in IPv6 land.</p>
<p>Specifically, <code>ICMPv6</code> is much more important than the IPv4 version that we use for pinging stuff and is used extensively for the <em><a href="https://en.wikipedia.org/wiki/Neighbor_Discovery_Protocol">Neighbor Discovery Protocol</a></em>, that&rsquo;s those &ldquo;Router Solicitation&rdquo; and &ldquo;Router Advertisement&rdquo; messages you see in the screenshot above.</p>
<p>So from this startup dump we can see:</p>
<ul>
<li>My ISP router inquires with ISP uplink through <em>Router Solicitation</em> where the uplink router is</li>
<li>Uplink responds with <em>Router Advertisement</em></li>
<li>ISP router advertises itself through DHCPv6, uses a DUID (&ldquo;DHCP unique identifier&rdquo;) to identify itself, this DUID is generated off the mac address</li>
<li>ISP responds to DHCPv6 request and assigns IPv6 prefix</li>
<li>Neighbor Discovery Protocol continues with Neighbor Solicitation and Neighbor Advertisement</li>
</ul>
<p>(the XID stuff you see in there is a transaction id that&rsquo;s being calculated on each request)</p>
<p><strong>So to summarize</strong>: This ISP is using fully native IPv6, the IPv6 prefix space is assigned through DHCPv6, and authentication happens through a DUID identifier that&rsquo;s being used in all DHCPv6 requests.</p>
<p>If we are able to replicate this handshake on the UDM, we should have IPv6 working. Now how do we actually do that&hellip;</p>
<h2 id="teaching-the-udm-how-to-dhcpv6">Teaching the UDM how to DHCPv6</h2>
<p>From the wireshark dump above, we know that the ISP router is using DHCPv6 to authenticate and get its prefix assigned. We also know which DUID the ISP router uses for identifying itself, and we know the prefix size that we get assigned from the ISP (/48).</p>
<p>Let&rsquo;s enable IPv6 through DHCPv6 through the UniFi admin on WAN:</p>
<p>
<img class="img-fluid" src="./ipv6-settings.png" alt="ipv6-settings" />

</p>
<p>Then grep for <code>dhcp</code> to see what&rsquo;s happening on the UDM:</p>
<pre tabindex="0"><code># ps aux | grep dhcp
 3537 root     /sbin/udhcpc --foreground --interface eth4 --script /usr/share/ubios-udapi-server/ubios-udhcpc-script -r 124.214.62.19
 3895 root     /usr/sbin/odhcp6c -R -f -e -v -s /usr/share/ubios-udapi-server/ubios-odhcp6c-script -P 48 eth4
 7715 root     grep dhcp
</code></pre><p>The UDM is spinning up <code>udhcpc</code> and <code>odhcp6c</code> with the closed-source binaries <code>ubios-odhcp6c-script</code>, both on the <code>eth4</code> interface (my WAN port).</p>
<p>We can also print our own <code>duid</code> by running <code>cat /config/dnsmasq.lease</code>, and since the DUID being used is not the one that our ISP router uses, we have to do something about it.</p>
<p><a href="https://github.com/openwrt/odhcp6c">odhcp6c</a> that is being used here is is a DHCPv6 client by openwrt and it being open-source and widely used means we can dig deeper into what it&rsquo;s doing with all those parameters:</p>
<pre tabindex="0"><code>Usage: odhcp6c [options] &lt;interface&gt;

Feature options:
        -S &lt;time&gt;       Wait at least &lt;time&gt; sec for a DHCP-server (0)
        -N &lt;mode&gt;       Mode for requesting addresses [try|force|none]
        -P &lt;length&gt;     Request IPv6-Prefix (0 = auto)
        -F              Force IPv6-Prefix
        -V &lt;class&gt;      Set vendor-class option (base-16 encoded)
        -u &lt;user-class&gt; Set user-class option string
        -c &lt;clientid&gt;   Override client-ID (base-16 encoded 16-bit type + value)
        -i &lt;iface-id&gt;   Use a custom interface identifier for RA handling
        -r &lt;options&gt;    Options to be requested (comma-separated)
        -R              Do not request any options except those specified with -r
        -s &lt;script&gt;     Status update script (/usr/sbin/odhcp6c-update)
        -a              Don&#39;t send Accept Reconfigure option
        -f              Don&#39;t send Client FQDN option
        -k              Don&#39;t send a RELEASE when stopping
        -t &lt;seconds&gt;    Maximum timeout for DHCPv6-SOLICIT (120)
        -m &lt;seconds&gt;    Minimum time between accepting updates (30)
</code></pre><p>The flag we&rsquo;re after is <code>-c &lt;clientid&gt;   Override client-ID (base-16 encoded 16-bit type + value) </code>. So theoretically, if we are able to pass <code>-c</code> into the startup of the <code>odhcp6c</code> command, we should be able to receive a valid IPv6 address from the ISP. Let&rsquo;s give it a try!</p>
<p>Disable DHCPv6 again through the admin UI, wait until the process fully stopped, then manually run the command above, but with <code>-c</code> flag:</p>
<pre tabindex="0"><code>/usr/sbin/odhcp6c -R -f -c &lt;DUID from our wireshark dump&gt;  -e -v -s /usr/share/ubios-udapi-server/ubios-odhcp6c-script -P 48 eth4
</code></pre><p>Wait a bit, then <code>ip -6</code></p>
<pre tabindex="0"><code># ip -6 addr show scope global
10: br0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 state UP qlen 1000
    inet6 fd74:d39f:a55f::1/64 scope global
       valid_lft forever preferred_lft forever
    inet6 240f:78:&lt;snip&gt;/64 scope global dynamic
       valid_lft 108367sec preferred_lft 108367sec
</code></pre><p>Hello there, IPv6 :)</p>
<p>Back on the UDM admin, let&rsquo;s enable IPv6 on the LAN network. We want to do prefix delegation and also advertise ourselves as a router (remember <em>Neighbor Discovery Protocol</em> above?)</p>
<p>
<img class="img-fluid" src="./ipv6-lan-settings.png" alt="ipv6-lan-settings" />

</p>
<p>Wait a bit, then let&rsquo;s enable IPv6 on my mac and renew the DHCPv6 lease:</p>
<pre tabindex="0"><code>en0: flags=8863&lt;UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST&gt; mtu 1500
        inet6 240f:78:9464::&lt;snip&gt; prefixlen 64 dynamic
        inet6 240f:78:9464:0:&lt;snip&gt; prefixlen 64 autoconf secured
        inet6 240f:78:9464:0:&lt;snip&gt; prefixlen 64 autoconf temporary
</code></pre><p>Nice!!</p>
<p>And what about <a href="https://test-ipv6.com">https://test-ipv6.com</a>?</p>
<p>
<img class="img-fluid" src="./ipv6-test.png" alt="ipv6-test" />

</p>
<h3 id="persisting-changes-to-the-udm">Persisting changes to the UDM</h3>
<p>Now that we got an IPv6 address, let&rsquo;s persist those changes so we don&rsquo;t have to manually run odhcpv6 every time.</p>
<h4 id="pre-req-extending-the-udm-with-boot-scripts">Pre-req: Extending the UDM with boot-scripts</h4>
<p>UniFi gear is <em>hackable</em> which allows us to do more than just what&rsquo;s possible through the admin UI. The USG had a very advanced customization flow baked in, but the UDM is a tad more annoying to work with.</p>
<p>Luckily there is this awesome project called <a href="https://github.com/boostchicken-dev/udm-utilities">udm-utilities</a> by boostchicken, a &ldquo;<em>A collection of things to enhance the capabilities of your Unifi Dream Machine or Dream Machine Pro.</em>&rdquo;</p>
<p>Basically what this project does is, it installs a custom package that extends the UDM with the ability to run scripts on boot that persist in between reboots. We put our shell scripts into <code>/mnt/data/on_boot.d</code>, and udm-utilities makes sure those are run when the router boots.</p>
<p>There are a lot of really cool things you can do with it, from configuring the UDM to even running podman containers like homebridge or adguard, but we&rsquo;ll be focusing on the network aspects only for this writeup.</p>
<h4 id="changing-duid-on-boot">Changing DUID on boot</h4>
<p>Now the next problem is, how do we actually start <code>odhcp6c</code> with our own options? The UDM launches it on boot through <code>ubios-udapi-server</code>, which is closed-source. We could decompile the binary with ghidra and see if we can hack our own arguments in, how hard can it be?</p>
<p>But there is an easier (more hacky) way of achieving just that. What if the binary at <code>/usr/sbin/odhcp6c</code> isn&rsquo;t actually a binary, but an executable script that runs <code>odhcp6c</code> for us? The UDM will continue to execute <code>odhcp6c</code> like usual and we can inject our custom flags. Here&rsquo;s what I mean:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># cat /usr/sbin/odhcp6c</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#!/bin/sh</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># workaround script to inject odhcp6 options</span>
</span></span><span style="display:flex;"><span>/usr/sbin/odhcp6c-real -R -f -c 0003000&lt;snip&gt; <span style="color:#e6db74">&#34;</span>$@<span style="color:#e6db74">&#34;</span>
</span></span></code></pre></div><p>The custom script that we put in <code>/usr/sbin</code> is calling <code>odhcp6c</code> transparently and adds the extra <code>-c</code> flag.</p>
<p>Using <code>udm-utilities</code>, we can automate this with the following boot script:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/sh
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>mv /usr/sbin/odhcp6c /usr/sbin/odhcp6c-real
</span></span><span style="display:flex;"><span>cp /mnt/data/odhcp6c/odhcp6c /usr/sbin/odhcp6c
</span></span><span style="display:flex;"><span>chmod +x /usr/sbin/odhcp6c
</span></span><span style="display:flex;"><span>killall odhcp6c
</span></span></code></pre></div><ol>
<li>We move <code>odhcp6c</code> to <code>odhcp6c-real</code></li>
<li>Copy the custom <code>odhcp6c</code> to the old location</li>
<li>Make it executable</li>
<li>Kill odhcp6c</li>
</ol>
<p>Once killed, <code>ubios-udapi-server</code> will automatically attempt to restart it by executing <code>/usr/sbin/odhcp6c</code>, which is no longer the real binary but our custom script.</p>
<p>And does it work?</p>
<pre tabindex="0"><code># ps aux | grep dhcp
 3891 root     {odhcp6c} /bin/sh /usr/sbin/odhcp6c -e -v -s /usr/share/ubios-udapi-server/ubios-odhcp6c-script -P 64 eth4
 3895 root     /usr/sbin/odhcp6c-real -R -f -c 0003000&lt;snip&gt; -e -v -s /usr/share/ubios-udapi-server/ubios-odhcp6c-script -P 64 eth4
</code></pre><p>It absolutely does!</p>
<h2 id="was-it-worth-it">Was it worth it?</h2>
<p>Worth it in terms of performance or network benefit? Probably not. The world doesn&rsquo;t feel different when interacting with IPv6 and speed for me is pretty much on par as it was with IPv4.</p>
<p>But it has been a very nice learning experience. Without this venture into IPv6-land I would have never learned about the differences between IPv6 and IPv4.</p>
<p>My network is now fully IPv6 compatible which it wasn&rsquo;t before, and that&rsquo;s an achievement in itself.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>My custom molded attenuating earplugs</title>
      <link>https://david.coffee/my-custom-molded-attenuating-earplugs/</link>
      <pubDate>Mon, 28 Mar 2022 11:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/my-custom-molded-attenuating-earplugs/</guid><media:content url="https://david.coffee/IMG_5636.jpg" medium="image" />
          <enclosure url="https://david.coffee/IMG_5636.jpg" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/IMG_5636.jpg&#34; /&gt;&lt;/p&gt;&lt;p&gt;A little backstory - Like most people I didn&amp;rsquo;t really take hearing protection serious until around my mid-twenties, when I realized that the faint high-pitched sound I hear when it&amp;rsquo;s very silent isn&amp;rsquo;t just my blood or body noises, but actual tinnitus.&lt;/p&gt;
&lt;p&gt;Luckily it&amp;rsquo;s so silent that I never actually notice it unless I actively try to focus on the sound. I also started doing regular hearing checks and hey - it turns out my hearing is still far better than people way younger my age. (I&amp;rsquo;m proud to still be able to hear that high-pitched beeping in front of department stores and get annoyed by it)&lt;/p&gt;
&lt;p&gt;But It was still an eye-opener.&lt;/p&gt;
&lt;p&gt;A couple years back when I attended a music festival, I started learning about &lt;em&gt;attenuating earplugs&lt;/em&gt; - or earplugs that don&amp;rsquo;t just seal the ear, but contain a small filter that &lt;em&gt;attenuates&lt;/em&gt; (fancy word for &lt;em&gt;lessens&lt;/em&gt;) the amount of sound that goes through them. I got a pair of &lt;a href=&#34;https://www.blox.info/en/&#34;&gt;blox&lt;/a&gt; ears-plugs from a booth that was available and have been using them religiously whenever I go to a club or festival ever since. The nice thing of attenuation over blocking is that you can still hear and hold conversations normally, for example.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./old%20earplugs.jpg&#34; alt=&#34;My Old Earplugs&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;These days I DJ a lot in clubs and am surrounded by loud sound more than I&amp;rsquo;d wanted to be. I usually keep my earplugs in at all time up until the moment I actually play, mainly because they still slightly distort the sound, but hey what do you expect from $30-50 plugs. I&amp;rsquo;m now taking music and DJing more serious, so after years of use I&amp;rsquo;ve decided I finally wanted something nicer.&lt;/p&gt;
&lt;h2 id=&#34;the-world-of-custom-molded-audio-things&#34;&gt;The world of custom-molded audio things&lt;/h2&gt;
&lt;p&gt;My world expanded after I went down the audiophile rabbit-hole. Turns out you can custom mold a bunch of things - from custom in-ear monitors, hearing aids, but also earplugs!&lt;/p&gt;
&lt;p&gt;There are a bunch of places that do custom orders for earplugs, from companies to music stores, to even just getting them at the local otolaryngologist made.&lt;/p&gt;
&lt;p&gt;I wanted something that was specifically made for &lt;em&gt;music&lt;/em&gt;, so something that protects my ears while keeping distortion to a minimum. Optimally something that artists use on stage (though that&amp;rsquo;s mostly IEMs). I singled out 2 companies that are known in this field, &lt;a href=&#34;https://www.acscustom.com/uk/&#34;&gt;ACS&lt;/a&gt; and &lt;a href=&#34;https://www.sensaphonics.jp/index.html&#34;&gt;sensaphonics&lt;/a&gt;, but ACS requires you to send in a scan of our ear-canal which I obviously didn&amp;rsquo;t have yet.&lt;/p&gt;
&lt;p&gt;Sensaphonics has a stronger presence in Japan full with Japanese website so that&amp;rsquo;s what I ended up picking. Even better, their &lt;a href=&#34;https://www.sensaphonics.jp/lineup/earplugs.html&#34;&gt;musicians earplugs&lt;/a&gt; support swappable filters in -9db, -15db, and -25db steps which make them very versatile. There are also graphs available that show how these earplugs specifically affect the sound in different frequencies so they were arguably the best option scientifically as well.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./5c252d339e93ae4cad3bc71c761563ea.webp&#34; alt=&#34;Graph published by e-earphone&#34; /&gt;

&lt;/p&gt;
&lt;h2 id=&#34;getting-a-ear-canal-impression-done&#34;&gt;Getting a ear-canal impression done&lt;/h2&gt;
&lt;p&gt;Contact and order was very painless through their &lt;a href=&#34;https://www.sensaphonics.jp/lineup/earplugs.html&#34;&gt;product page&lt;/a&gt;. They sent through a questionnaire by email, confirmed the order details and asked me to pick a location where I&amp;rsquo;d like to do my ear-canal impression. Sensaphonics doesn&amp;rsquo;t do this themselves, but delegate it to local medical stores that have the necessary equipment for making hearing-aids.&lt;/p&gt;
&lt;p&gt;The impression is done by filling the ear with a playdoh-like substance that hardens after a few minutes.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./IMG_5488.jpg&#34; alt=&#34;My ear impression&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Besides the odd feeling of having your ear completely 100% sealed, this is painless.&lt;/p&gt;
&lt;p&gt;The impression is then sent to a lab where it&amp;rsquo;s scanned and digitized, then sent to the manufacturer to be used for the mold. I asked if I could have access to the scan data, but Sensaphonics made it clear that they wouldn&amp;rsquo;t do that&lt;/p&gt;
&lt;p&gt;And that was it. All that was left was a 3 weeks wait.&lt;/p&gt;
&lt;h2 id=&#34;my-custom-molded-attenuating-earplugs&#34;&gt;My custom molded attenuating earplugs&lt;/h2&gt;
&lt;p&gt;3 weeks later and we finally arrived at the title of this blog post - my earplugs arrived!&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./IMG_5636.jpg&#34; alt=&#34;My Custom Earplugs&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;The earplugs feel well made. For some reason didn&amp;rsquo;t expect them to be so squishy, but it makes sense because otherwise how would they seal the ear without damaging it? They go &lt;em&gt;all the way&lt;/em&gt; in and fit perfectly. Not uncomfortable, not hard to put in, it just fits.&lt;/p&gt;
&lt;p&gt;I went with the -15db filter for my club and DJ adventures but am considering getting the -9db filter as well for more casual events. I am a little worried about the swappable filter thing, and eventual wear around the insertion point. Swapping them too often will probably cause it to get loose and allow more sound go through the cracks.&lt;/p&gt;
&lt;p&gt;But how do they actually sound?? And how do these $200+ custom molded ones compare to the &amp;lt;$50 blox I used until now??&lt;/p&gt;
&lt;p&gt;Well they sound&amp;hellip; normal, duh. Everything sounds the same as it always does, just a bit quieter. I trialed them at a very loud club and sometimes even forgot I had them in. Post-club, no ear ringing so I call that a success!&lt;/p&gt;
&lt;p&gt;Compared to the blox, a couple things - first of all they are different db levels. My custom ones are -15db where the blox were -19db. On the blox, things sound more muffled. Not bad, just more muffled and quiet. They have a stronger &amp;lsquo;&lt;em&gt;earplug feeling&lt;/em&gt;&amp;rsquo;, and putting them in is harder.&lt;/p&gt;
&lt;h2 id=&#34;so-should-you-go-out-and-get-custom-ones-made-as-well&#34;&gt;So, should you go out and get custom ones made as well?&lt;/h2&gt;
&lt;p&gt;The answer is probably &amp;rsquo;no&amp;rsquo;, but you absolutely should get attenuating earplugs if you are exposed to louder sounds semi-regularly.&lt;/p&gt;
&lt;p&gt;Hearing loss is permanent (for now). There is currently no cure for tinnitus and bringing back what has been destroyed, so taking care of your ears is very important. Don&amp;rsquo;t act reactive when you started noticing loss, but proactive and try to prevent it.&lt;/p&gt;
&lt;p&gt;If you have the cash and want the best of the best, then yes, get custom molded ones. They are definitely nice and I like mine a lot.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/IMG_5636.jpg" /></p><p>A little backstory - Like most people I didn&rsquo;t really take hearing protection serious until around my mid-twenties, when I realized that the faint high-pitched sound I hear when it&rsquo;s very silent isn&rsquo;t just my blood or body noises, but actual tinnitus.</p>
<p>Luckily it&rsquo;s so silent that I never actually notice it unless I actively try to focus on the sound. I also started doing regular hearing checks and hey - it turns out my hearing is still far better than people way younger my age. (I&rsquo;m proud to still be able to hear that high-pitched beeping in front of department stores and get annoyed by it)</p>
<p>But It was still an eye-opener.</p>
<p>A couple years back when I attended a music festival, I started learning about <em>attenuating earplugs</em> - or earplugs that don&rsquo;t just seal the ear, but contain a small filter that <em>attenuates</em> (fancy word for <em>lessens</em>) the amount of sound that goes through them. I got a pair of <a href="https://www.blox.info/en/">blox</a> ears-plugs from a booth that was available and have been using them religiously whenever I go to a club or festival ever since. The nice thing of attenuation over blocking is that you can still hear and hold conversations normally, for example.</p>
<p>
<img class="img-fluid" src="./old%20earplugs.jpg" alt="My Old Earplugs" />

</p>
<p>These days I DJ a lot in clubs and am surrounded by loud sound more than I&rsquo;d wanted to be. I usually keep my earplugs in at all time up until the moment I actually play, mainly because they still slightly distort the sound, but hey what do you expect from $30-50 plugs. I&rsquo;m now taking music and DJing more serious, so after years of use I&rsquo;ve decided I finally wanted something nicer.</p>
<h2 id="the-world-of-custom-molded-audio-things">The world of custom-molded audio things</h2>
<p>My world expanded after I went down the audiophile rabbit-hole. Turns out you can custom mold a bunch of things - from custom in-ear monitors, hearing aids, but also earplugs!</p>
<p>There are a bunch of places that do custom orders for earplugs, from companies to music stores, to even just getting them at the local otolaryngologist made.</p>
<p>I wanted something that was specifically made for <em>music</em>, so something that protects my ears while keeping distortion to a minimum. Optimally something that artists use on stage (though that&rsquo;s mostly IEMs). I singled out 2 companies that are known in this field, <a href="https://www.acscustom.com/uk/">ACS</a> and <a href="https://www.sensaphonics.jp/index.html">sensaphonics</a>, but ACS requires you to send in a scan of our ear-canal which I obviously didn&rsquo;t have yet.</p>
<p>Sensaphonics has a stronger presence in Japan full with Japanese website so that&rsquo;s what I ended up picking. Even better, their <a href="https://www.sensaphonics.jp/lineup/earplugs.html">musicians earplugs</a> support swappable filters in -9db, -15db, and -25db steps which make them very versatile. There are also graphs available that show how these earplugs specifically affect the sound in different frequencies so they were arguably the best option scientifically as well.</p>
<p>
<img class="img-fluid" src="./5c252d339e93ae4cad3bc71c761563ea.webp" alt="Graph published by e-earphone" />

</p>
<h2 id="getting-a-ear-canal-impression-done">Getting a ear-canal impression done</h2>
<p>Contact and order was very painless through their <a href="https://www.sensaphonics.jp/lineup/earplugs.html">product page</a>. They sent through a questionnaire by email, confirmed the order details and asked me to pick a location where I&rsquo;d like to do my ear-canal impression. Sensaphonics doesn&rsquo;t do this themselves, but delegate it to local medical stores that have the necessary equipment for making hearing-aids.</p>
<p>The impression is done by filling the ear with a playdoh-like substance that hardens after a few minutes.</p>
<p>
<img class="img-fluid" src="./IMG_5488.jpg" alt="My ear impression" />

</p>
<p>Besides the odd feeling of having your ear completely 100% sealed, this is painless.</p>
<p>The impression is then sent to a lab where it&rsquo;s scanned and digitized, then sent to the manufacturer to be used for the mold. I asked if I could have access to the scan data, but Sensaphonics made it clear that they wouldn&rsquo;t do that</p>
<p>And that was it. All that was left was a 3 weeks wait.</p>
<h2 id="my-custom-molded-attenuating-earplugs">My custom molded attenuating earplugs</h2>
<p>3 weeks later and we finally arrived at the title of this blog post - my earplugs arrived!</p>
<p>
<img class="img-fluid" src="./IMG_5636.jpg" alt="My Custom Earplugs" />

</p>
<p>The earplugs feel well made. For some reason didn&rsquo;t expect them to be so squishy, but it makes sense because otherwise how would they seal the ear without damaging it? They go <em>all the way</em> in and fit perfectly. Not uncomfortable, not hard to put in, it just fits.</p>
<p>I went with the -15db filter for my club and DJ adventures but am considering getting the -9db filter as well for more casual events. I am a little worried about the swappable filter thing, and eventual wear around the insertion point. Swapping them too often will probably cause it to get loose and allow more sound go through the cracks.</p>
<p>But how do they actually sound?? And how do these $200+ custom molded ones compare to the &lt;$50 blox I used until now??</p>
<p>Well they sound&hellip; normal, duh. Everything sounds the same as it always does, just a bit quieter. I trialed them at a very loud club and sometimes even forgot I had them in. Post-club, no ear ringing so I call that a success!</p>
<p>Compared to the blox, a couple things - first of all they are different db levels. My custom ones are -15db where the blox were -19db. On the blox, things sound more muffled. Not bad, just more muffled and quiet. They have a stronger &lsquo;<em>earplug feeling</em>&rsquo;, and putting them in is harder.</p>
<h2 id="so-should-you-go-out-and-get-custom-ones-made-as-well">So, should you go out and get custom ones made as well?</h2>
<p>The answer is probably &rsquo;no&rsquo;, but you absolutely should get attenuating earplugs if you are exposed to louder sounds semi-regularly.</p>
<p>Hearing loss is permanent (for now). There is currently no cure for tinnitus and bringing back what has been destroyed, so taking care of your ears is very important. Don&rsquo;t act reactive when you started noticing loss, but proactive and try to prevent it.</p>
<p>If you have the cash and want the best of the best, then yes, get custom molded ones. They are definitely nice and I like mine a lot.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Some tiny personal programs I&#39;ve written</title>
      <link>https://david.coffee/some-tiny-personal-programs-i-ve-written/</link>
      <pubDate>Sat, 19 Mar 2022 11:01:23 +0000</pubDate>
      
      <guid>https://david.coffee/some-tiny-personal-programs-i-ve-written/</guid><description>&lt;p&gt;There was this post on HN the other day titled &lt;a href=&#34;https://jvns.ca/blog/2022/03/08/tiny-programs/&#34;&gt;Some tiny personal programs I&amp;rsquo;ve written&lt;/a&gt; that I enjoyed a lot. It resonated with me so much because I&amp;rsquo;ve been pumping out smaller programs left and right for a while as well, but never thought of sharing those with the public, so let&amp;rsquo;s change that.&lt;/p&gt;
&lt;p&gt;I love hacking on mini projects. Fixing a problem with programming feels like a superpower - I can do something that other people that can&amp;rsquo;t program can&amp;rsquo;t do, and every time I find a situation where I can apply my skills to an issue, I feel happy.&lt;/p&gt;
&lt;p&gt;Here are some programs I&amp;rsquo;ve written in the past few months, in no specific order:&lt;/p&gt;
&lt;h2 id=&#34;a-covid-bot&#34;&gt;a covid bot&lt;/h2&gt;
&lt;p&gt;Every day when there were new cases published, people posted Twitter links, links to news and other sources. Turns out Tokyo has an &lt;a href=&#34;https://stopcovid19.metro.tokyo.lg.jp/&#34;&gt;official dashboard&lt;/a&gt; that pulls data out of a GitHub repository and is updated daily.&lt;/p&gt;
&lt;p&gt;About an hour after discovering those json files, we had a slackbot that posts updates once daily - nice!&lt;/p&gt;
&lt;img src=&#34;./coronachan.png&#34;  /&gt;
&lt;h2 id=&#34;apartment-finder-line-bot&#34;&gt;apartment finder LINE bot&lt;/h2&gt;
&lt;p&gt;This one I&amp;rsquo;m especially proud of because I was able to get my current place thanks to it.&lt;/p&gt;
&lt;p&gt;In Tokyo there are 2 &amp;lsquo;companies&amp;rsquo; that are owned by the government, and rent out pretty decent places for an ok price, without hidden costs, minimun contract runtime, cancellation fee, and so on. Only problem - these place are gone &lt;em&gt;fast&lt;/em&gt;. Like in minutes after something is posted, it&amp;rsquo;s gone.&lt;/p&gt;
&lt;p&gt;People don&amp;rsquo;t even bother visiting these places, they just put in an application right away, then visit them and cancel accordingly.&lt;/p&gt;
&lt;p&gt;At first I was checking the page every couple hours once, but that was too inefficient, so I hacked together a bot that&amp;rsquo;s using &lt;a href=&#34;https://github.com/puppeteer/puppeteer&#34;&gt;puppeteer&lt;/a&gt; to check for new place, and send me a message on LINE whenever something new is available. A couple weeks later and my dream place free&amp;rsquo;d up!&lt;/p&gt;
&lt;img src=&#34;./apatochan.jpeg&#34; alt=&#34;Apartment bot in action&#34; style=&#34;zoom: 33%;&#34; /&gt;
&lt;h2 id=&#34;fastmail-masked-aliases-through-the-cli&#34;&gt;Fastmail masked aliases through the cli&lt;/h2&gt;
&lt;p&gt;I wrote a &lt;a href=&#34;https://david.coffee/creating-fastmail-masked-emails-without-1password-anytime-and-everywhere&#34;&gt;separate blogpost&lt;/a&gt; about this one so I&amp;rsquo;ll skip the details, but when Fastmail + 1Password released their &amp;lsquo;masked email&amp;rsquo; (domain specific email addresses) implementation I was stoked because I was doing something like this for a long long time. I wanted to migrate to this new feature, but it wasn&amp;rsquo;t available to the public yet, and only usable through 1Password.&lt;/p&gt;
&lt;p&gt;A put a couple hours in to reverse-engineer the fastmail JMAP API with proxyman and hacked together a Golang client that was able to create identical requests. Now I have masked emails everywhere I want by just hammering &amp;lsquo;@@@&amp;rsquo; into a dialogue.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;https://blotcdn.com/blog_f1c3613082cc4491b8eddec3b73ef2fe/_image_cache/3f698dde-7569-4a97-af6a-95825955d3e8.gif&#34; alt=&#34;Fastmail masked emails through Keyboard Maestro&#34; /&gt;

&lt;/p&gt;
&lt;h2 id=&#34;work-hour-tracking-through-the-cli&#34;&gt;work-hour tracking through the cli&lt;/h2&gt;
&lt;p&gt;At work we use this tool called &lt;a href=&#34;https://www.kingoftime.jp/&#34;&gt;King of Time&lt;/a&gt; to track working hours. It&amp;rsquo;s usable through the web app but doing bulk edits was very tedious. I wanted to be able to hook checkins/checkouts to different events and actions, and the lack of an official API made this difficult.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/puppeteer/puppeteer&#34;&gt;puppeteer&lt;/a&gt; to the rescue (it involved way too much javascript to be usable without headless browser), and tadaa - a npm-installable company internal tool to interact with King of Time was born.&lt;/p&gt;
&lt;img src=&#34;./kot-cli.png&#34; alt=&#34;The time-tracking CLI&#34; style=&#34;zoom:50%;&#34; /&gt;
&lt;h2 id=&#34;auto-importing-japanese-words-into-my-study-app&#34;&gt;auto-importing Japanese words into my study app&lt;/h2&gt;
&lt;p&gt;When I was learning Japanese more actively I often encountered words I didn&amp;rsquo;t know, as you usually do. I looked those up in a dictionary, and then usually added them to my flashcard app (Anki).&lt;/p&gt;
&lt;p&gt;This became repetitive fast, so I did what engineers do and hacked together a &lt;a href=&#34;https://github.com/dvcrn/ankimo&#34;&gt;chrome extension&lt;/a&gt; that does it for me. Bonus points for writing it in ClojureScript which I wanted to get better with at that time.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;https://github.com/dvcrn/ankimo/raw/master/screenshots/tangorin.gif&#34; alt=&#34;ankimo&#34; /&gt;

&lt;/p&gt;
&lt;h2 id=&#34;auto-compound-crypto&#34;&gt;auto-compound crypto&lt;/h2&gt;
&lt;p&gt;This one is the definition of a micro-program. FTX is able to lend away unused crypto and gives interest back. I wanted this interest to be auto-compounded every hour without me having to open the site all the time and click some buttons.&lt;/p&gt;
&lt;p&gt;Luckily FTX has an API to do just that, so hooking it up to a script that runs in cron, and we have automatic compounding :)&lt;/p&gt;
&lt;h2 id=&#34;homekit-ifying-my-tv-and-aircon&#34;&gt;HomeKit-ifying my TV and aircon&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://homebridge.io/&#34;&gt;homebridge&lt;/a&gt; is an amazing piece of software. It allows to integrate basically anything that has an API into Apples HomeKit, which is what I use at home.&lt;/p&gt;
&lt;p&gt;Problem: Neither my Air-Conditioner, nor my TV are HomeKit-compatible. The TV is from a time way before HomeKit was even a thing (running &amp;rsquo;netcast&amp;rsquo;), and my Air-Conditioner is only using through Sharps proprietory &amp;lsquo;Cocoro Cloud&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;2 homebridge plugins got created during longer caffeinated sessions, and now I can automate both purely in HomeKit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/dvcrn/homebridge-netcast-tv&#34;&gt;https://github.com/dvcrn/homebridge-netcast-tv&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/dvcrn/homebridge-sharp-cocoro&#34;&gt;https://github.com/dvcrn/homebridge-sharp-cocoro&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;netatmo-temperature-getter&#34;&gt;netatmo temperature getter&lt;/h2&gt;
&lt;p&gt;I use a netatmo weather station at home and wanted to get it&amp;rsquo;s temperature to integrate into HomeKit, so I can always see what the current temperature is, but also automate things like my AirCon or fans. This is another micro-script that just pulls the stations from my account and stores it in a text file (&lt;code&gt;/tmp/temperature&lt;/code&gt;), running in a CRON&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;station&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;range&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;dc&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Stations&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;_&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;module&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;range&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;station&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Modules&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;module&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ID&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;homeModuleID&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;				&lt;span style=&#34;color:#a6e22e&#34;&gt;fmt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Println&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Found home module&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;				&lt;span style=&#34;color:#a6e22e&#34;&gt;homeModule&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;module&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;				&lt;span style=&#34;color:#66d9ef&#34;&gt;break&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;temperature&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fmt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Sprintf&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;%.1f&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;homeModule&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;DashboardData&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Temperature&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;fmt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Printf&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Current temperature at home: %v \n&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;temperature&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;ioutil&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WriteFile&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/tmp/temperature&amp;#34;&lt;/span&gt;, []byte(&lt;span style=&#34;color:#a6e22e&#34;&gt;temperature&lt;/span&gt;), &lt;span style=&#34;color:#ae81ff&#34;&gt;0644&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;-and-lots-more&#34;&gt;&amp;hellip; and lots more!&lt;/h2&gt;
&lt;p&gt;It&amp;rsquo;s nice to just hack on things in between professional tasks and work. Low commitment, easy to explore new technology and usually makes something in my day-to-day life a bit easier.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>There was this post on HN the other day titled <a href="https://jvns.ca/blog/2022/03/08/tiny-programs/">Some tiny personal programs I&rsquo;ve written</a> that I enjoyed a lot. It resonated with me so much because I&rsquo;ve been pumping out smaller programs left and right for a while as well, but never thought of sharing those with the public, so let&rsquo;s change that.</p>
<p>I love hacking on mini projects. Fixing a problem with programming feels like a superpower - I can do something that other people that can&rsquo;t program can&rsquo;t do, and every time I find a situation where I can apply my skills to an issue, I feel happy.</p>
<p>Here are some programs I&rsquo;ve written in the past few months, in no specific order:</p>
<h2 id="a-covid-bot">a covid bot</h2>
<p>Every day when there were new cases published, people posted Twitter links, links to news and other sources. Turns out Tokyo has an <a href="https://stopcovid19.metro.tokyo.lg.jp/">official dashboard</a> that pulls data out of a GitHub repository and is updated daily.</p>
<p>About an hour after discovering those json files, we had a slackbot that posts updates once daily - nice!</p>
<img src="./coronachan.png"  />
<h2 id="apartment-finder-line-bot">apartment finder LINE bot</h2>
<p>This one I&rsquo;m especially proud of because I was able to get my current place thanks to it.</p>
<p>In Tokyo there are 2 &lsquo;companies&rsquo; that are owned by the government, and rent out pretty decent places for an ok price, without hidden costs, minimun contract runtime, cancellation fee, and so on. Only problem - these place are gone <em>fast</em>. Like in minutes after something is posted, it&rsquo;s gone.</p>
<p>People don&rsquo;t even bother visiting these places, they just put in an application right away, then visit them and cancel accordingly.</p>
<p>At first I was checking the page every couple hours once, but that was too inefficient, so I hacked together a bot that&rsquo;s using <a href="https://github.com/puppeteer/puppeteer">puppeteer</a> to check for new place, and send me a message on LINE whenever something new is available. A couple weeks later and my dream place free&rsquo;d up!</p>
<img src="./apatochan.jpeg" alt="Apartment bot in action" style="zoom: 33%;" />
<h2 id="fastmail-masked-aliases-through-the-cli">Fastmail masked aliases through the cli</h2>
<p>I wrote a <a href="https://david.coffee/creating-fastmail-masked-emails-without-1password-anytime-and-everywhere">separate blogpost</a> about this one so I&rsquo;ll skip the details, but when Fastmail + 1Password released their &lsquo;masked email&rsquo; (domain specific email addresses) implementation I was stoked because I was doing something like this for a long long time. I wanted to migrate to this new feature, but it wasn&rsquo;t available to the public yet, and only usable through 1Password.</p>
<p>A put a couple hours in to reverse-engineer the fastmail JMAP API with proxyman and hacked together a Golang client that was able to create identical requests. Now I have masked emails everywhere I want by just hammering &lsquo;@@@&rsquo; into a dialogue.</p>
<p>
<img class="img-fluid" src="https://blotcdn.com/blog_f1c3613082cc4491b8eddec3b73ef2fe/_image_cache/3f698dde-7569-4a97-af6a-95825955d3e8.gif" alt="Fastmail masked emails through Keyboard Maestro" />

</p>
<h2 id="work-hour-tracking-through-the-cli">work-hour tracking through the cli</h2>
<p>At work we use this tool called <a href="https://www.kingoftime.jp/">King of Time</a> to track working hours. It&rsquo;s usable through the web app but doing bulk edits was very tedious. I wanted to be able to hook checkins/checkouts to different events and actions, and the lack of an official API made this difficult.</p>
<p><a href="https://github.com/puppeteer/puppeteer">puppeteer</a> to the rescue (it involved way too much javascript to be usable without headless browser), and tadaa - a npm-installable company internal tool to interact with King of Time was born.</p>
<img src="./kot-cli.png" alt="The time-tracking CLI" style="zoom:50%;" />
<h2 id="auto-importing-japanese-words-into-my-study-app">auto-importing Japanese words into my study app</h2>
<p>When I was learning Japanese more actively I often encountered words I didn&rsquo;t know, as you usually do. I looked those up in a dictionary, and then usually added them to my flashcard app (Anki).</p>
<p>This became repetitive fast, so I did what engineers do and hacked together a <a href="https://github.com/dvcrn/ankimo">chrome extension</a> that does it for me. Bonus points for writing it in ClojureScript which I wanted to get better with at that time.</p>
<p>
<img class="img-fluid" src="https://github.com/dvcrn/ankimo/raw/master/screenshots/tangorin.gif" alt="ankimo" />

</p>
<h2 id="auto-compound-crypto">auto-compound crypto</h2>
<p>This one is the definition of a micro-program. FTX is able to lend away unused crypto and gives interest back. I wanted this interest to be auto-compounded every hour without me having to open the site all the time and click some buttons.</p>
<p>Luckily FTX has an API to do just that, so hooking it up to a script that runs in cron, and we have automatic compounding :)</p>
<h2 id="homekit-ifying-my-tv-and-aircon">HomeKit-ifying my TV and aircon</h2>
<p><a href="https://homebridge.io/">homebridge</a> is an amazing piece of software. It allows to integrate basically anything that has an API into Apples HomeKit, which is what I use at home.</p>
<p>Problem: Neither my Air-Conditioner, nor my TV are HomeKit-compatible. The TV is from a time way before HomeKit was even a thing (running &rsquo;netcast&rsquo;), and my Air-Conditioner is only using through Sharps proprietory &lsquo;Cocoro Cloud&rsquo;.</p>
<p>2 homebridge plugins got created during longer caffeinated sessions, and now I can automate both purely in HomeKit:</p>
<ul>
<li>
<p><a href="https://github.com/dvcrn/homebridge-netcast-tv">https://github.com/dvcrn/homebridge-netcast-tv</a></p>
</li>
<li>
<p><a href="https://github.com/dvcrn/homebridge-sharp-cocoro">https://github.com/dvcrn/homebridge-sharp-cocoro</a></p>
</li>
</ul>
<h2 id="netatmo-temperature-getter">netatmo temperature getter</h2>
<p>I use a netatmo weather station at home and wanted to get it&rsquo;s temperature to integrate into HomeKit, so I can always see what the current temperature is, but also automate things like my AirCon or fans. This is another micro-script that just pulls the stations from my account and stores it in a text file (<code>/tmp/temperature</code>), running in a CRON</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">station</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">dc</span>.<span style="color:#a6e22e">Stations</span>() {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">module</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">station</span>.<span style="color:#a6e22e">Modules</span>() {
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">module</span>.<span style="color:#a6e22e">ID</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">homeModuleID</span> {
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;Found home module&#34;</span>)
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">homeModule</span> = <span style="color:#a6e22e">module</span>
</span></span><span style="display:flex;"><span>				<span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>			}
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">temperature</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">&#34;%.1f&#34;</span>, <span style="color:#f92672">*</span><span style="color:#a6e22e">homeModule</span>.<span style="color:#a6e22e">DashboardData</span>.<span style="color:#a6e22e">Temperature</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;Current temperature at home: %v \n&#34;</span>, <span style="color:#a6e22e">temperature</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">WriteFile</span>(<span style="color:#e6db74">&#34;/tmp/temperature&#34;</span>, []byte(<span style="color:#a6e22e">temperature</span>), <span style="color:#ae81ff">0644</span>)
</span></span></code></pre></div><h2 id="-and-lots-more">&hellip; and lots more!</h2>
<p>It&rsquo;s nice to just hack on things in between professional tasks and work. Low commitment, easy to explore new technology and usually makes something in my day-to-day life a bit easier.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Anybox: Productive little bookmark manager</title>
      <link>https://david.coffee/anybox-productive-little-bookmark-manager/</link>
      <pubDate>Tue, 15 Mar 2022 19:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/anybox-productive-little-bookmark-manager/</guid><media:content url="https://david.coffee/anybox%20overview.png" medium="image" />
          <enclosure url="https://david.coffee/anybox%20overview.png" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/anybox%20overview.png&#34; /&gt;&lt;/p&gt;&lt;p&gt;Ran into this little app called &lt;a href=&#34;https://anybox.cc/&#34;&gt;Anybox&lt;/a&gt; the other day while I was hunting for bookmark managers to try and ended up liking this one a lot&lt;/p&gt;
&lt;h2 id=&#34;how-i-was-doing-things-until-now&#34;&gt;How I was doing things until now&lt;/h2&gt;
&lt;p&gt;Up until now, my way to manage bookmarks was to, well&amp;hellip;, just bookmark them in my browser (duh), then use Alfred Workflows like &lt;a href=&#34;https://github.com/mdreizin/chrome-bookmarks-alfred-workflow&#34;&gt;this one&lt;/a&gt; (or more recently, just Raycast) to give me a dialog that can fuzzy complete bookmarks by name whenever I hit &lt;code&gt;CMD-CTRL-B&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./raycast.png&#34; alt=&#34;Raycast Bookmark filter&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;This started to feel a little insufficient when I switched from Software Engineer to Engineering Manager. All of a sudden I had so many links to &lt;em&gt;things&lt;/em&gt; that I had to somehow keep track of - meeting documents, 1on1s, project overviews, Wiki pages with information on how to handle situations like employee resignation, and and and. The browser autocomplete for bookmarks worked, but I still often lost pages and had to re-find them in an ocean of slack messages, especially when the browser history got cleared.&lt;/p&gt;
&lt;p&gt;I knew of &lt;a href=&#34;https://raindrop.io/&#34;&gt;raindrop.io&lt;/a&gt; and while it looks super sleek, it never clicked and I felt like I was trying to force myself to use it, without having a problem for it to solve. Instead of putting bookmarks into my browser, I was putting them into raindrop which just felt slower in every way.&lt;/p&gt;
&lt;p&gt;What I wanted was what I already had: Some way to rapidly bookmark things, then get a &lt;em&gt;Quick Open&lt;/em&gt; dialogue like in Sublime Text or Obsidian to fuzzy-find stuff. I also wanted it to be browser independent, so I could stop re-importing all my bookmarks every time I switch main browsers. Basically, I wanted a bookmark manager that is built for busy people that are used to features of IDEs.&lt;/p&gt;
&lt;p&gt;Turns out that&amp;rsquo;s what Anybox is!&lt;/p&gt;
&lt;h2 id=&#34;anybox&#34;&gt;Anybox&lt;/h2&gt;
&lt;p&gt;Anybox is a mix between bookmark manager and &amp;lsquo;store everything&amp;rsquo; app like DEVONthink (though far less advanced). It&amp;rsquo;s an app to, well, clip and manage bookmarks (duh).&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./anybox%20overview.png&#34; alt=&#34;Anybox&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Anybox is an &lt;em&gt;invisible app&lt;/em&gt; to me. I barely ever interact with it through it&amp;rsquo;s main window, but what I do all the time is hit &lt;code&gt;CMD-CTRL-B&lt;/code&gt; to conjure a textbox in the middle of my screen that lets me fuzzy-filter over all my bookmarks&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./Screen%20Shot%202022-03-15%20at%2018.39.08.png&#34; alt=&#34;Anybox Quick Open&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;It syncs through iCloud, it can quickly open bookmarks, it allows me to clip from any browser by right-clicking on something, and as a bonus: It has an iOS app that allows me to do the same! Neat!&lt;/p&gt;
&lt;img src=&#34;./anybox-right-click.png&#34; alt=&#34;Anybox Context-Menu Action&#34; style=&#34;zoom:33%;&#34; /&gt;
&lt;p&gt;Of course Anybox can do a lot more. It has support for other file-types like images (well the name is &amp;lsquo;&lt;em&gt;Anybox&lt;/em&gt;&amp;rsquo; and not &amp;lsquo;&lt;em&gt;BookmarkBox&lt;/em&gt;&amp;rsquo;) and can even automatically archive bookmarks as PDF, Screen Shot and even Web Archive if you need that.&lt;/p&gt;
&lt;p&gt;Everything in Anybox is built with an engineers mindset: &lt;code&gt;CMD-P&lt;/code&gt; opens a quick switcher to jump around the app, &lt;code&gt;CMD-K&lt;/code&gt; opens a Sublime-like command palette to do all the actions through a keyboard-interface&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./anybox%20command%20palette.png&#34; alt=&#34;Command Palette in action&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;There are things I&amp;rsquo;m not happy with, mainly that using authenticated sites that require login/password are hard to deal with. In the beginning there were other issues with clipping from Firefox/Chrome and retaining of things like the site title (all my clips ended up becoming &amp;ldquo;Please login to JIRA&amp;rdquo;), but after a quick chat with the developer on twitter, he pushed an update to fix those things literally &lt;em&gt;the next day&lt;/em&gt;! (I bought a lifetime license right afterwards even though I don&amp;rsquo;t use any of the pro features)&lt;/p&gt;
&lt;p&gt;The dev has also added AppleScript automation support after users requested it and is in general very open to feature requests and suggestions.&lt;/p&gt;
&lt;p&gt;So, do I really &lt;em&gt;need&lt;/em&gt; a bookmark manager? Until recently I was convinced that the answer is a clear &amp;rsquo;no&amp;rsquo;. Important stuff goes into DEVONthink for archiving, and for the rest, the browser autocomplete and bookmark management features are sufficient.&lt;/p&gt;
&lt;p&gt;But Anybox is one of those things that doesn&amp;rsquo;t feel like it&amp;rsquo;s changing my workflow a bit. It&amp;rsquo;s intuitive and fits right into how I want-, and were doing things already anyway, so this one gets a big thumbs up.&lt;/p&gt;
&lt;p&gt;Get it for free from &lt;a href=&#34;https://anybox.cc/&#34;&gt;anybox.cc&lt;/a&gt;. Subscriptions are $1.49 a month, $11.99 a year or $27.99 lifetime&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/anybox%20overview.png" /></p><p>Ran into this little app called <a href="https://anybox.cc/">Anybox</a> the other day while I was hunting for bookmark managers to try and ended up liking this one a lot</p>
<h2 id="how-i-was-doing-things-until-now">How I was doing things until now</h2>
<p>Up until now, my way to manage bookmarks was to, well&hellip;, just bookmark them in my browser (duh), then use Alfred Workflows like <a href="https://github.com/mdreizin/chrome-bookmarks-alfred-workflow">this one</a> (or more recently, just Raycast) to give me a dialog that can fuzzy complete bookmarks by name whenever I hit <code>CMD-CTRL-B</code>.</p>
<p>
<img class="img-fluid" src="./raycast.png" alt="Raycast Bookmark filter" />

</p>
<p>This started to feel a little insufficient when I switched from Software Engineer to Engineering Manager. All of a sudden I had so many links to <em>things</em> that I had to somehow keep track of - meeting documents, 1on1s, project overviews, Wiki pages with information on how to handle situations like employee resignation, and and and. The browser autocomplete for bookmarks worked, but I still often lost pages and had to re-find them in an ocean of slack messages, especially when the browser history got cleared.</p>
<p>I knew of <a href="https://raindrop.io/">raindrop.io</a> and while it looks super sleek, it never clicked and I felt like I was trying to force myself to use it, without having a problem for it to solve. Instead of putting bookmarks into my browser, I was putting them into raindrop which just felt slower in every way.</p>
<p>What I wanted was what I already had: Some way to rapidly bookmark things, then get a <em>Quick Open</em> dialogue like in Sublime Text or Obsidian to fuzzy-find stuff. I also wanted it to be browser independent, so I could stop re-importing all my bookmarks every time I switch main browsers. Basically, I wanted a bookmark manager that is built for busy people that are used to features of IDEs.</p>
<p>Turns out that&rsquo;s what Anybox is!</p>
<h2 id="anybox">Anybox</h2>
<p>Anybox is a mix between bookmark manager and &lsquo;store everything&rsquo; app like DEVONthink (though far less advanced). It&rsquo;s an app to, well, clip and manage bookmarks (duh).</p>
<p>
<img class="img-fluid" src="./anybox%20overview.png" alt="Anybox" />

</p>
<p>Anybox is an <em>invisible app</em> to me. I barely ever interact with it through it&rsquo;s main window, but what I do all the time is hit <code>CMD-CTRL-B</code> to conjure a textbox in the middle of my screen that lets me fuzzy-filter over all my bookmarks</p>
<p>
<img class="img-fluid" src="./Screen%20Shot%202022-03-15%20at%2018.39.08.png" alt="Anybox Quick Open" />

</p>
<p>It syncs through iCloud, it can quickly open bookmarks, it allows me to clip from any browser by right-clicking on something, and as a bonus: It has an iOS app that allows me to do the same! Neat!</p>
<img src="./anybox-right-click.png" alt="Anybox Context-Menu Action" style="zoom:33%;" />
<p>Of course Anybox can do a lot more. It has support for other file-types like images (well the name is &lsquo;<em>Anybox</em>&rsquo; and not &lsquo;<em>BookmarkBox</em>&rsquo;) and can even automatically archive bookmarks as PDF, Screen Shot and even Web Archive if you need that.</p>
<p>Everything in Anybox is built with an engineers mindset: <code>CMD-P</code> opens a quick switcher to jump around the app, <code>CMD-K</code> opens a Sublime-like command palette to do all the actions through a keyboard-interface</p>
<p>
<img class="img-fluid" src="./anybox%20command%20palette.png" alt="Command Palette in action" />

</p>
<p>There are things I&rsquo;m not happy with, mainly that using authenticated sites that require login/password are hard to deal with. In the beginning there were other issues with clipping from Firefox/Chrome and retaining of things like the site title (all my clips ended up becoming &ldquo;Please login to JIRA&rdquo;), but after a quick chat with the developer on twitter, he pushed an update to fix those things literally <em>the next day</em>! (I bought a lifetime license right afterwards even though I don&rsquo;t use any of the pro features)</p>
<p>The dev has also added AppleScript automation support after users requested it and is in general very open to feature requests and suggestions.</p>
<p>So, do I really <em>need</em> a bookmark manager? Until recently I was convinced that the answer is a clear &rsquo;no&rsquo;. Important stuff goes into DEVONthink for archiving, and for the rest, the browser autocomplete and bookmark management features are sufficient.</p>
<p>But Anybox is one of those things that doesn&rsquo;t feel like it&rsquo;s changing my workflow a bit. It&rsquo;s intuitive and fits right into how I want-, and were doing things already anyway, so this one gets a big thumbs up.</p>
<p>Get it for free from <a href="https://anybox.cc/">anybox.cc</a>. Subscriptions are $1.49 a month, $11.99 a year or $27.99 lifetime</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>How I manage my digital life in 2022 (feat. Obsidian, DEVONthink &amp; Friends)</title>
      <link>https://david.coffee/how-i-manage-my-digital-life-in-2022-feat-obsidian-devonthink-friends/</link>
      <pubDate>Tue, 01 Feb 2022 10:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/how-i-manage-my-digital-life-in-2022-feat-obsidian-devonthink-friends/</guid><media:content url="https://david.coffee/remarkable-today.jpg" medium="image" />
          <enclosure url="https://david.coffee/remarkable-today.jpg" length="0" type="image/jpeg" /><description>&lt;p&gt;&lt;img src=&#34;https://david.coffee/remarkable-today.jpg&#34; /&gt;&lt;/p&gt;&lt;p&gt;Today, let&amp;rsquo;s talk a little about my favorite time-sink: &lt;em&gt;Productivity&lt;/em&gt;!&lt;/p&gt;
&lt;p&gt;I can&amp;rsquo;t even count how often I re-invented my entire information management stack on the hunt for the perfect solution. Tweaks here, tweaks there - putting some new app into the center focus and building everything around it (again).&lt;/p&gt;
&lt;p&gt;Did I find the perfect solution? Nah, I don&amp;rsquo;t think it actually exists. On the contrary, tools that give me too many customization options turned out to be a pretty bad match for a brain like mine.&lt;/p&gt;
&lt;p&gt;So I took it slower. I stopped overthinking the hell out of the perfect system and just let the software handle things for me. Over the course of the past months/years this resulted in a system that works incredibly well for me that I&amp;rsquo;ll be using to start 2022.&lt;/p&gt;
&lt;p&gt;Today, let&amp;rsquo;s talk about these apps and what role they play in my day to day life. First on the list we have&amp;hellip;&lt;/p&gt;
&lt;h2 id=&#34;1-devonthink&#34;&gt;1) DEVONthink&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://www.devontechnologies.com/apps/devonthink&#34;&gt;DEVONthink&lt;/a&gt; has been my gem for the past I think 3 years. It&amp;rsquo;s maybe the most used, most important Mac app that I own. It&amp;rsquo;s also the most expensive app I&amp;rsquo;ve purchased so far.&lt;/p&gt;
&lt;p&gt;When friends hear me rave about DEVONthink and they go and check it out, they usually end up even more confused about what it actually is than before.&lt;/p&gt;
&lt;p&gt;DEVONthink is a tool for organizing information, but it&amp;rsquo;s one of these things that is something else for each person that&amp;rsquo;s using it: For researchers it could be a tool to clip content from the web and aggregate information. For academics it could be a database to classify and link content together. For me it&amp;rsquo;s my digital office.&lt;/p&gt;
&lt;p&gt;Each document I receive like PDFs, images and scans go into DEVONthink. And I really mean &lt;em&gt;each document&lt;/em&gt;: When I get a physical letter I scan (not OCR) it with &lt;a href=&#34;https://thegrizzlylabs.com/genius-scan/&#34;&gt;Genius Scan&lt;/a&gt; and share it into DEVONthink through the iOS share sheet. When I plan a trip, I clip the PDFs for flight reservations and hotels into DEVONthink. When my cat gets a health-check, the document goes into DEVONthink, and so on. In DEVONthink I then use the built-in Abbyy OCR engine (Abbyy is arguably still king in OCR land) to process the documents, then classify and organize them.&lt;/p&gt;
&lt;p&gt;DEVONthink is powerful enough that just by my habits of where certain documents go, and by looking at the content of new documents, it can already tell me with good accuracy where a document likely belongs. All I have to do is hit &lt;code&gt;Ctrl-C&lt;/code&gt; and DEVONthink moves it for me. DEVONthink also warns me when I have duplicates, tells me about similar documents and puts logic into my huge pile of documents. No matter what I&amp;rsquo;m searching for, even if it&amp;rsquo;s some tax receipt from 3 years ago, I know exactly how to find it.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./devonthink-1.png&#34; alt=&#34;DEVONthink classification in action&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;But another even more important reason why DEVONthink is in the middle of my digital life is its sync capabilities: DEVONthink allows me to setup &lt;em&gt;multiple&lt;/em&gt; parallel sync targets like iCloud, Dropbox, Bonjour, WebDav, &lt;strong&gt;each with their own encryption key&lt;/strong&gt;! So my Database could be stored in different locations, fully e2ee encrypted with different keys. No other software I have used is able to do this, which puts it miles ahead of directly using Dropbox/iCloud Drive for me. It gives me peace of mind that my data is secure and backed-up properly with a fail-safe.&lt;/p&gt;
&lt;p&gt;In an age where every cloud provider is processing your data for their ML models, having true e2ee systems is rare.&lt;/p&gt;
&lt;h2 id=&#34;2-obsidian&#34;&gt;2) Obsidian&lt;/h2&gt;
&lt;p&gt;I tried hard to use DEVONthink for notes and quick scribbles but the UI just doesn&amp;rsquo;t do it for me. It always felt awkward, like I&amp;rsquo;m working in a pile of .txt documents with a barebones notepad editor. I wanted something nicer, and after hopping through dozens of different apps (notable mention to Outlinely and Agenda) I somehow ended up at &lt;a href=&#34;https://obsidian.md/&#34;&gt;Obsidian&lt;/a&gt;. Ironic because Obsidian is another one of those apps that are hard to actually &amp;lsquo;&lt;em&gt;get&lt;/em&gt;&amp;rsquo;, and surprising because I usually stay far away from electron-based apps. Yet Obsidian became a staple in my life.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d describe Obsidian as a &lt;em&gt;knowledge management&lt;/em&gt; app. Didn&amp;rsquo;t I just say the same thing about DEVONthink? Kinda, both apps have some overlap in what they do. The website says &amp;ldquo;Obsidian is a powerful knowledge base on top of a local folder of plain text Markdown files.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a markdown editor that stores the files in a database (a &amp;ldquo;&lt;em&gt;vault&lt;/em&gt;&amp;rdquo;), and gives you a Quick Open dialog. You can also customize literally &lt;em&gt;everything&lt;/em&gt; about the app with plugins (I even wrote &lt;a href=&#34;https://github.com/dvcrn/obsidian-filename-heading-sync/&#34;&gt;my own plugin&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./obsidian-3.png&#34; alt=&#34;Obsidian Quick Open&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Obsidian can become very complex if you spend too much time tweaking it, or trying to follow the productivity system of some internet guru that swear by it. It&amp;rsquo;s very easy to fall into this habit, I myself am a bit guilty of that. &lt;strong&gt;The best way to experience Obsidian is to just use it&lt;/strong&gt; without thinking what you want to adjust and tweak. Don&amp;rsquo;t find a problem to solve with it, instead let your brain embrace it. This sounds preachy but I mean it! Just install it and see if it clicks or not.&lt;/p&gt;
&lt;p&gt;I mentioned above that my brain is prone to overthink, over-tweak and over-optimize everything and that also happened when I started to use Obsidian. Honestly, it even stressed me.&lt;/p&gt;
&lt;p&gt;What changed this was when I went through a very busy period in my life and had to just write things down quickly. I had no time to think about folders, tags, links and so on. I just wanted to write things down and do the rest &lt;em&gt;later&lt;/em&gt;.
Turns out that this was exactly what my brain needs - a dump of information that I can access at my fingertips the second I need to recall that information.&lt;/p&gt;
&lt;p&gt;My brain doesn&amp;rsquo;t care a bit if my &amp;ldquo;&lt;em&gt;House Music Sub-genres Differences&lt;/em&gt;&amp;rdquo; note is in a folder called &lt;code&gt;music/&lt;/code&gt;, &lt;code&gt;dj/&lt;/code&gt; or even &lt;code&gt;music/dj/&lt;/code&gt; (see where I&amp;rsquo;m going with this?), because I will never access the note that way. What I will do is hit &lt;code&gt;CMD-O&lt;/code&gt; and hammer in &amp;ldquo;&lt;em&gt;house differences&lt;/em&gt;&amp;rdquo;, and boom - Obsidian gives me exactly what I wanted to find.&lt;/p&gt;
&lt;p&gt;It takes away all the mental overhead of having to find a proper directory structure, tagging system or what not and allows me to focus on exactly what I want to focus on: The information.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./obsidian-1.png&#34; alt=&#34;Obsidian File Overview&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;I label all my notes by the question that note answers, for example &amp;ldquo;How to do hardlinks in APFS&amp;rdquo; or &amp;ldquo;How to construct Minor Chords&amp;rdquo;. If there is structure between notes, I also insert a link and maybe add them to a Map of Content, like a &amp;ldquo;Music MOC&amp;rdquo; that pulls together certain pieces of information with relation.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./obsidian-2.png&#34; alt=&#34;Obsidian MOC Example&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;I never have to worry that I&amp;rsquo;ll forget certain bits of information because Obsidian got me covered. Obsidian is free, but I also pay for &lt;a href=&#34;https://obsidian.md/sync&#34;&gt;Obsidian Sync&lt;/a&gt;. The main reason for that is, you might have guessed it, e2ee encryption! Syncing with Obsidian Sync means my data is stored in encrypted form with a key set by me. If I were to store the Obsidian vault in iCloud Drive for example, it would again technically be accessible by Apple.&lt;/p&gt;
&lt;h2 id=&#34;3-things--to-do-management&#34;&gt;3) Things &amp;amp; To-Do management&lt;/h2&gt;
&lt;p&gt;Believe it or not, I hate todo-list apps the most so I&amp;rsquo;ll keep this short. I tried every todo management solution under the sun, but it comes down to the same issue: I am not actually doing what I put in my todo list. No clever OmniFocus-like system was able to change that.&lt;/p&gt;
&lt;p&gt;I treat my todo-list app as a reminder to not forget something. When I have a thought, I immediately log it on my iPhone or with Siri, then later remember about the item once I open the app again. It takes the fear of forgetting off my brain.&lt;/p&gt;
&lt;p&gt;The app I am using this year, the same as last year and the year before is &lt;a href=&#34;https://culturedcode.com/things/&#34;&gt;Things&lt;/a&gt;. It&amp;rsquo;s the most simple and (arguably) best-looking option out there and fits my workflow pretty well. It&amp;rsquo;s also non-subscription which I value a lot.&lt;/p&gt;
&lt;p&gt;Nothing too fancy here, I have areas of responsibility for private, work, productivity and shopping. When I get an email that I need to act on, I immediately use the quick-entry shortcut to create a todo out of it, so I don&amp;rsquo;t forget.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./things-quick-entry.png&#34; alt=&#34;Things Quick Entry&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Maybe one &lt;em&gt;productivity hack&lt;/em&gt; I am using is to use Zapier to automatically create new todos whenever someone assigns me a pull request on GitHub or issue on JIRA. Sometimes emails about PRs or mentions on GitHub go under, so automating it was the next step to put a bandaid on that issue.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./things-gh-pr.png&#34; alt=&#34;GitHub PR in Things&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;I then prioritize the items, move them into my Today list when appropriate, and usually work out of whatever is in Today. Every couple days I go through &lt;em&gt;all&lt;/em&gt; items in Things and look at them again, to make sure there isn&amp;rsquo;t something I am forgetting.&lt;/p&gt;
&lt;p&gt;Now there is still the problem that I&amp;rsquo;m often not doing all the things that I put in my &lt;em&gt;Today&lt;/em&gt; list. Sometimes it&amp;rsquo;s because lack of time, sometimes there are too many items, sometimes I&amp;rsquo;m just too busy. Sometimes there are other excuses. When you see the same items day after day, your brain (or at least my brain) starts to ignore them. They are still &lt;em&gt;in&lt;/em&gt; the todo list, but my brain starts considering them &lt;em&gt;part&lt;/em&gt; of the todo list, if that makes any sense.&lt;/p&gt;
&lt;p&gt;I &lt;a href=&#34;https://bulletjournal.com/&#34;&gt;BulletJournal-ed&lt;/a&gt; in the past with an actual paper journal which was nice for keeping focus on certain things. This year I wanted to experiment with bringing some of that feeling back, without having to carry paper and pen with me all the time, so I picked up a ReMarkable2 tablet.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./remarkable-today.jpg&#34; alt=&#34;ReMarkable Day Planner&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Not much to say about it yet besides that it&amp;rsquo;s an experiment. The logic of using a handwritten day-planner over a todo list is that I have to make the conscious decision of actually writing an item into the planner, with my hand. And I have to do that for each and every day that I decide that I want to do that item. If I don&amp;rsquo;t end up doing it on that day, yup, write it down again. That puts emphasis on the things I actually really want to do, and gives me an out if I decide something is no longer important.&lt;/p&gt;
&lt;p&gt;(It also just feels nice to use a pen)&lt;/p&gt;
&lt;h2 id=&#34;4-other-little-things&#34;&gt;4) Other little things&lt;/h2&gt;
&lt;p&gt;I think those are the main big tools that I&amp;rsquo;ll be using for the foreseeable future. I might expand this post with other things if something else pops up, but a quick scan over my iPhone and currently running Mac apps confirms me that there isn&amp;rsquo;t much else to go into big detail.&lt;/p&gt;
&lt;p&gt;A few bits and pieces though:&lt;/p&gt;
&lt;h3 id=&#34;41-arq&#34;&gt;4.1) Arq&lt;/h3&gt;
&lt;p&gt;I think without &lt;a href=&#34;https://www.arqbackup.com/&#34;&gt;Arq&lt;/a&gt;, I wouldn&amp;rsquo;t be able to properly sleep at night. My Mac has a 2TB disk, and all of that huge amount of data could potentially vanish in an instant if I were to drop it. Arq allows me to backup e2ee to different providers: I always have a local backup on my SSD, and then one in the cloud. I also backup Obsidian, DEVONthink and everything else I consider important up. Arq saved me so many times that it&amp;rsquo;s just fair to include here.&lt;/p&gt;
&lt;h3 id=&#34;42-syncthing&#34;&gt;4.2) Syncthing&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://syncthing.net/&#34;&gt;Syncthing&lt;/a&gt; is a newer tool I&amp;rsquo;m currently trialing. After buying that new 2TB Macbook Pro, I now have 2 main devices: A mac mini and a Macbook. Syncthing allows me to quickly sync folders across those 2 machines without having to touch the internet, fully P2P. Most of my stuff syncs e2ee, and DEVONthink already does P2P with Bonjour sync, so it&amp;rsquo;s more one of these things that come in handy now and then when I have to shovel big amounts of data around.&lt;/p&gt;
&lt;h2 id=&#34;and-as-usual&#34;&gt;And as usual&amp;hellip;&lt;/h2&gt;
&lt;p&gt;&amp;hellip;these tools work incredibly well for &lt;em&gt;me&lt;/em&gt;, but that obviously doesn&amp;rsquo;t mean they will for you as well. I&amp;rsquo;m just glad that I reached a point where I don&amp;rsquo;t have to waste further mental energy to adjust or tweak the system.&lt;/p&gt;
&lt;p&gt;I know where to put things, how to access them and how to easily recall information again. I have peace of mind that my stuff is safe and secure for years to come, and that&amp;rsquo;s all I want.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><img src="https://david.coffee/remarkable-today.jpg" /></p><p>Today, let&rsquo;s talk a little about my favorite time-sink: <em>Productivity</em>!</p>
<p>I can&rsquo;t even count how often I re-invented my entire information management stack on the hunt for the perfect solution. Tweaks here, tweaks there - putting some new app into the center focus and building everything around it (again).</p>
<p>Did I find the perfect solution? Nah, I don&rsquo;t think it actually exists. On the contrary, tools that give me too many customization options turned out to be a pretty bad match for a brain like mine.</p>
<p>So I took it slower. I stopped overthinking the hell out of the perfect system and just let the software handle things for me. Over the course of the past months/years this resulted in a system that works incredibly well for me that I&rsquo;ll be using to start 2022.</p>
<p>Today, let&rsquo;s talk about these apps and what role they play in my day to day life. First on the list we have&hellip;</p>
<h2 id="1-devonthink">1) DEVONthink</h2>
<p><a href="https://www.devontechnologies.com/apps/devonthink">DEVONthink</a> has been my gem for the past I think 3 years. It&rsquo;s maybe the most used, most important Mac app that I own. It&rsquo;s also the most expensive app I&rsquo;ve purchased so far.</p>
<p>When friends hear me rave about DEVONthink and they go and check it out, they usually end up even more confused about what it actually is than before.</p>
<p>DEVONthink is a tool for organizing information, but it&rsquo;s one of these things that is something else for each person that&rsquo;s using it: For researchers it could be a tool to clip content from the web and aggregate information. For academics it could be a database to classify and link content together. For me it&rsquo;s my digital office.</p>
<p>Each document I receive like PDFs, images and scans go into DEVONthink. And I really mean <em>each document</em>: When I get a physical letter I scan (not OCR) it with <a href="https://thegrizzlylabs.com/genius-scan/">Genius Scan</a> and share it into DEVONthink through the iOS share sheet. When I plan a trip, I clip the PDFs for flight reservations and hotels into DEVONthink. When my cat gets a health-check, the document goes into DEVONthink, and so on. In DEVONthink I then use the built-in Abbyy OCR engine (Abbyy is arguably still king in OCR land) to process the documents, then classify and organize them.</p>
<p>DEVONthink is powerful enough that just by my habits of where certain documents go, and by looking at the content of new documents, it can already tell me with good accuracy where a document likely belongs. All I have to do is hit <code>Ctrl-C</code> and DEVONthink moves it for me. DEVONthink also warns me when I have duplicates, tells me about similar documents and puts logic into my huge pile of documents. No matter what I&rsquo;m searching for, even if it&rsquo;s some tax receipt from 3 years ago, I know exactly how to find it.</p>
<p>
<img class="img-fluid" src="./devonthink-1.png" alt="DEVONthink classification in action" />

</p>
<p>But another even more important reason why DEVONthink is in the middle of my digital life is its sync capabilities: DEVONthink allows me to setup <em>multiple</em> parallel sync targets like iCloud, Dropbox, Bonjour, WebDav, <strong>each with their own encryption key</strong>! So my Database could be stored in different locations, fully e2ee encrypted with different keys. No other software I have used is able to do this, which puts it miles ahead of directly using Dropbox/iCloud Drive for me. It gives me peace of mind that my data is secure and backed-up properly with a fail-safe.</p>
<p>In an age where every cloud provider is processing your data for their ML models, having true e2ee systems is rare.</p>
<h2 id="2-obsidian">2) Obsidian</h2>
<p>I tried hard to use DEVONthink for notes and quick scribbles but the UI just doesn&rsquo;t do it for me. It always felt awkward, like I&rsquo;m working in a pile of .txt documents with a barebones notepad editor. I wanted something nicer, and after hopping through dozens of different apps (notable mention to Outlinely and Agenda) I somehow ended up at <a href="https://obsidian.md/">Obsidian</a>. Ironic because Obsidian is another one of those apps that are hard to actually &lsquo;<em>get</em>&rsquo;, and surprising because I usually stay far away from electron-based apps. Yet Obsidian became a staple in my life.</p>
<p>I&rsquo;d describe Obsidian as a <em>knowledge management</em> app. Didn&rsquo;t I just say the same thing about DEVONthink? Kinda, both apps have some overlap in what they do. The website says &ldquo;Obsidian is a powerful knowledge base on top of a local folder of plain text Markdown files.&rdquo;</p>
<p>It&rsquo;s a markdown editor that stores the files in a database (a &ldquo;<em>vault</em>&rdquo;), and gives you a Quick Open dialog. You can also customize literally <em>everything</em> about the app with plugins (I even wrote <a href="https://github.com/dvcrn/obsidian-filename-heading-sync/">my own plugin</a>).</p>
<p>
<img class="img-fluid" src="./obsidian-3.png" alt="Obsidian Quick Open" />

</p>
<p>Obsidian can become very complex if you spend too much time tweaking it, or trying to follow the productivity system of some internet guru that swear by it. It&rsquo;s very easy to fall into this habit, I myself am a bit guilty of that. <strong>The best way to experience Obsidian is to just use it</strong> without thinking what you want to adjust and tweak. Don&rsquo;t find a problem to solve with it, instead let your brain embrace it. This sounds preachy but I mean it! Just install it and see if it clicks or not.</p>
<p>I mentioned above that my brain is prone to overthink, over-tweak and over-optimize everything and that also happened when I started to use Obsidian. Honestly, it even stressed me.</p>
<p>What changed this was when I went through a very busy period in my life and had to just write things down quickly. I had no time to think about folders, tags, links and so on. I just wanted to write things down and do the rest <em>later</em>.
Turns out that this was exactly what my brain needs - a dump of information that I can access at my fingertips the second I need to recall that information.</p>
<p>My brain doesn&rsquo;t care a bit if my &ldquo;<em>House Music Sub-genres Differences</em>&rdquo; note is in a folder called <code>music/</code>, <code>dj/</code> or even <code>music/dj/</code> (see where I&rsquo;m going with this?), because I will never access the note that way. What I will do is hit <code>CMD-O</code> and hammer in &ldquo;<em>house differences</em>&rdquo;, and boom - Obsidian gives me exactly what I wanted to find.</p>
<p>It takes away all the mental overhead of having to find a proper directory structure, tagging system or what not and allows me to focus on exactly what I want to focus on: The information.</p>
<p>
<img class="img-fluid" src="./obsidian-1.png" alt="Obsidian File Overview" />

</p>
<p>I label all my notes by the question that note answers, for example &ldquo;How to do hardlinks in APFS&rdquo; or &ldquo;How to construct Minor Chords&rdquo;. If there is structure between notes, I also insert a link and maybe add them to a Map of Content, like a &ldquo;Music MOC&rdquo; that pulls together certain pieces of information with relation.</p>
<p>
<img class="img-fluid" src="./obsidian-2.png" alt="Obsidian MOC Example" />

</p>
<p>I never have to worry that I&rsquo;ll forget certain bits of information because Obsidian got me covered. Obsidian is free, but I also pay for <a href="https://obsidian.md/sync">Obsidian Sync</a>. The main reason for that is, you might have guessed it, e2ee encryption! Syncing with Obsidian Sync means my data is stored in encrypted form with a key set by me. If I were to store the Obsidian vault in iCloud Drive for example, it would again technically be accessible by Apple.</p>
<h2 id="3-things--to-do-management">3) Things &amp; To-Do management</h2>
<p>Believe it or not, I hate todo-list apps the most so I&rsquo;ll keep this short. I tried every todo management solution under the sun, but it comes down to the same issue: I am not actually doing what I put in my todo list. No clever OmniFocus-like system was able to change that.</p>
<p>I treat my todo-list app as a reminder to not forget something. When I have a thought, I immediately log it on my iPhone or with Siri, then later remember about the item once I open the app again. It takes the fear of forgetting off my brain.</p>
<p>The app I am using this year, the same as last year and the year before is <a href="https://culturedcode.com/things/">Things</a>. It&rsquo;s the most simple and (arguably) best-looking option out there and fits my workflow pretty well. It&rsquo;s also non-subscription which I value a lot.</p>
<p>Nothing too fancy here, I have areas of responsibility for private, work, productivity and shopping. When I get an email that I need to act on, I immediately use the quick-entry shortcut to create a todo out of it, so I don&rsquo;t forget.</p>
<p>
<img class="img-fluid" src="./things-quick-entry.png" alt="Things Quick Entry" />

</p>
<p>Maybe one <em>productivity hack</em> I am using is to use Zapier to automatically create new todos whenever someone assigns me a pull request on GitHub or issue on JIRA. Sometimes emails about PRs or mentions on GitHub go under, so automating it was the next step to put a bandaid on that issue.</p>
<p>
<img class="img-fluid" src="./things-gh-pr.png" alt="GitHub PR in Things" />

</p>
<p>I then prioritize the items, move them into my Today list when appropriate, and usually work out of whatever is in Today. Every couple days I go through <em>all</em> items in Things and look at them again, to make sure there isn&rsquo;t something I am forgetting.</p>
<p>Now there is still the problem that I&rsquo;m often not doing all the things that I put in my <em>Today</em> list. Sometimes it&rsquo;s because lack of time, sometimes there are too many items, sometimes I&rsquo;m just too busy. Sometimes there are other excuses. When you see the same items day after day, your brain (or at least my brain) starts to ignore them. They are still <em>in</em> the todo list, but my brain starts considering them <em>part</em> of the todo list, if that makes any sense.</p>
<p>I <a href="https://bulletjournal.com/">BulletJournal-ed</a> in the past with an actual paper journal which was nice for keeping focus on certain things. This year I wanted to experiment with bringing some of that feeling back, without having to carry paper and pen with me all the time, so I picked up a ReMarkable2 tablet.</p>
<p>
<img class="img-fluid" src="./remarkable-today.jpg" alt="ReMarkable Day Planner" />

</p>
<p>Not much to say about it yet besides that it&rsquo;s an experiment. The logic of using a handwritten day-planner over a todo list is that I have to make the conscious decision of actually writing an item into the planner, with my hand. And I have to do that for each and every day that I decide that I want to do that item. If I don&rsquo;t end up doing it on that day, yup, write it down again. That puts emphasis on the things I actually really want to do, and gives me an out if I decide something is no longer important.</p>
<p>(It also just feels nice to use a pen)</p>
<h2 id="4-other-little-things">4) Other little things</h2>
<p>I think those are the main big tools that I&rsquo;ll be using for the foreseeable future. I might expand this post with other things if something else pops up, but a quick scan over my iPhone and currently running Mac apps confirms me that there isn&rsquo;t much else to go into big detail.</p>
<p>A few bits and pieces though:</p>
<h3 id="41-arq">4.1) Arq</h3>
<p>I think without <a href="https://www.arqbackup.com/">Arq</a>, I wouldn&rsquo;t be able to properly sleep at night. My Mac has a 2TB disk, and all of that huge amount of data could potentially vanish in an instant if I were to drop it. Arq allows me to backup e2ee to different providers: I always have a local backup on my SSD, and then one in the cloud. I also backup Obsidian, DEVONthink and everything else I consider important up. Arq saved me so many times that it&rsquo;s just fair to include here.</p>
<h3 id="42-syncthing">4.2) Syncthing</h3>
<p><a href="https://syncthing.net/">Syncthing</a> is a newer tool I&rsquo;m currently trialing. After buying that new 2TB Macbook Pro, I now have 2 main devices: A mac mini and a Macbook. Syncthing allows me to quickly sync folders across those 2 machines without having to touch the internet, fully P2P. Most of my stuff syncs e2ee, and DEVONthink already does P2P with Bonjour sync, so it&rsquo;s more one of these things that come in handy now and then when I have to shovel big amounts of data around.</p>
<h2 id="and-as-usual">And as usual&hellip;</h2>
<p>&hellip;these tools work incredibly well for <em>me</em>, but that obviously doesn&rsquo;t mean they will for you as well. I&rsquo;m just glad that I reached a point where I don&rsquo;t have to waste further mental energy to adjust or tweak the system.</p>
<p>I know where to put things, how to access them and how to easily recall information again. I have peace of mind that my stuff is safe and secure for years to come, and that&rsquo;s all I want.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>d.sh</title>
      <link>https://david.coffee/d-sh/</link>
      <pubDate>Tue, 25 Jan 2022 11:14:00 +0000</pubDate>
      
      <guid>https://david.coffee/d-sh/</guid><description>&lt;p&gt;Never expected to own a single-letter ccTLD/TLD (not gTLD), but it somehow happened anyway - yours truly is now the owner of &lt;strong&gt;d.sh&lt;/strong&gt;!&lt;/p&gt;
&lt;p&gt;A while back (must have been years by now) I scouted some short domain names to potentially use, one of them being d.sh. It&amp;rsquo;s a reserved premium name that became available and is now being exclusively handled by &lt;a href=&#34;www.divido.org/&#34;&gt;divido.org&lt;/a&gt;.
It had one of those &amp;ldquo;&lt;em&gt;This is a premium name&lt;/em&gt;&amp;rdquo; sites with an option to get notified when it&amp;rsquo;s being auctioned.&lt;/p&gt;
&lt;p&gt;Well a couple days ago I received an email that d.sh is now being auctioned off so I put in a casual offer, never actually expecting a reply back, but got word a couple days later that I actually won it - who would&amp;rsquo;ve thought.&lt;/p&gt;
&lt;p&gt;Now the bigger question is what to actually do with it&amp;hellip;.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>Never expected to own a single-letter ccTLD/TLD (not gTLD), but it somehow happened anyway - yours truly is now the owner of <strong>d.sh</strong>!</p>
<p>A while back (must have been years by now) I scouted some short domain names to potentially use, one of them being d.sh. It&rsquo;s a reserved premium name that became available and is now being exclusively handled by <a href="www.divido.org/">divido.org</a>.
It had one of those &ldquo;<em>This is a premium name</em>&rdquo; sites with an option to get notified when it&rsquo;s being auctioned.</p>
<p>Well a couple days ago I received an email that d.sh is now being auctioned off so I put in a casual offer, never actually expecting a reply back, but got word a couple days later that I actually won it - who would&rsquo;ve thought.</p>
<p>Now the bigger question is what to actually do with it&hellip;.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>VPN connections and local network traffic on Mac</title>
      <link>https://david.coffee/vpn-connections-and-local-network-traffic-on-mac/</link>
      <pubDate>Fri, 21 Jan 2022 14:19:00 +0000</pubDate>
      
      <guid>https://david.coffee/vpn-connections-and-local-network-traffic-on-mac/</guid><description>&lt;p&gt;These days I&amp;rsquo;ve been trying to be more conscious about my online footprint and privacy in general. I&amp;rsquo;ve been using one-off email addresses for years now (btw check out my recent post on &lt;a href=&#34;https://david.coffee/creating-fastmail-masked-emails-without-1password-anytime-and-everywhere&#34;&gt;masked emails&lt;/a&gt; if you haven&amp;rsquo;t yet), and have a bunch of little scripts that spit out things like random usernames when I hit &amp;ldquo;!&amp;rdquo; 3 times.&lt;/p&gt;
&lt;p&gt;But honestly speaking, IP addresses were one of those things I just kind of didn&amp;rsquo;t really care about.&lt;/p&gt;
&lt;p&gt;What changed my view drastically was when I hacked on a &lt;a href=&#34;https://workers.cloudflare.com/&#34;&gt;cloudflare workers&lt;/a&gt; project and for debug purpose decided to &lt;code&gt;console.log()&lt;/code&gt; the Request object. Here&amp;rsquo;s what got printed&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;message&amp;#34;&lt;/span&gt;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;cf&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;longitude&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;REDACTED&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;latitude&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;REDACTED&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;continent&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;AS&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;country&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;JP&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;tlsVersion&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;TLSv1.3&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;colo&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;NRT&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;timezone&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Asia/Tokyo&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;city&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;REDACTED&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;edgeRequestKeepAliveStatus&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;requestPriority&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;httpProtocol&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;HTTP/3&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;region&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Tokyo&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;regionCode&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;13&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;asOrganization&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;REDACTED&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;postalCode&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;REDACTED&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I cut out some of the details here, but the request had longitude, latitude, my internet provider name and a postal code that is &lt;em&gt;very&lt;/em&gt; close to mine. Meaning &lt;em&gt;every site&lt;/em&gt; I interact with basically knows where I live to a certain degree. That ain&amp;rsquo;t good.&lt;/p&gt;
&lt;p&gt;I now have a VPN running almost all of the time, which brings me to the main topic of this post:&lt;/p&gt;
&lt;p&gt;When using a VPN, like NordVPN, and you want to let that run 24/7 so that you can move all traffic without leaks through the it, you&amp;rsquo;ll sooner or later end up running into issues with things like local network no longer resolving because it&amp;rsquo;s not part of the virtual network.&lt;/p&gt;
&lt;p&gt;How do you fix that?&lt;/p&gt;
&lt;h2 id=&#34;temporary-rules-with-route&#34;&gt;Temporary rules with route&lt;/h2&gt;
&lt;p&gt;Mac has a bunch of neat network tools that you can use to get around this, the easiest is &lt;code&gt;route&lt;/code&gt;:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;# route &amp;lt;target&amp;gt; &amp;lt;gateway&amp;gt;
sudo route -nv add -net 10.0 192.168.1.1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;with the &lt;code&gt;-net&lt;/code&gt; flag (there is also &lt;code&gt;-host&lt;/code&gt;), &lt;code&gt;10.0&lt;/code&gt; expands to &lt;code&gt;10.0.0.0&lt;/code&gt;, so the entire subnet of &lt;code&gt;10.0.*&lt;/code&gt; will get routed to the gateway &lt;code&gt;192.168.1.1&lt;/code&gt; instead of the VPN network, sweet! (10.0.5.* is my network for services and smaller devices)&lt;/p&gt;
&lt;p&gt;Routes added with &lt;code&gt;route&lt;/code&gt; are temporary, so once you restart they&amp;rsquo;re gone which makes them good for experimenting and quickly setting things up in a non-destructive way.&lt;/p&gt;
&lt;h2 id=&#34;making-things-persist&#34;&gt;Making things persist&lt;/h2&gt;
&lt;p&gt;For persistent rules, mac comes with the &lt;code&gt;networksetup&lt;/code&gt; tool:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;❯ networksetup -listallnetworkservices

An asterisk (*) denotes that a network service is disabled.
Ethernet
USB 10/100/1000 LAN
Wi-Fi
Thunderbolt Bridge
NordVPN NordLynx
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The command we want is &lt;code&gt;-setadditionalroutes&lt;/code&gt;. This command takes a interface, and then triplets of &lt;code&gt;&amp;lt;target&amp;gt; &amp;lt;netmask&amp;gt; &amp;lt;gateway&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;networksetup -setadditionalroutes &amp;#34;Ethernet&amp;#34; 10.0.5.0 255.255.255.0 192.168.1.1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This command adds a route for &lt;code&gt;10.0.5.*&lt;/code&gt; (designated by the netmask &lt;code&gt;255.255.255.0&lt;/code&gt;) and routes all traffic to the gateway &lt;code&gt;192.168.1.1&lt;/code&gt; instead.&lt;/p&gt;
&lt;p&gt;Repeat that for all your interfaces that you use to connect, and done.&lt;/p&gt;
&lt;p&gt;To add multiple routes you&amp;rsquo;ll have to specify them in the same go, in triplets:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;networksetup -setadditionalroutes &amp;#34;Ethernet&amp;#34; 192.168.0.0 255.255.0.0 192.168.1.1 10.0.5.0 255.255.255.0 192.168.1.1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now everything from &lt;code&gt;192.168.*&lt;/code&gt; and &lt;code&gt;10.0.5.*&lt;/code&gt; will get routed to my router&lt;/p&gt;
&lt;h4 id=&#34;about-nordvpn&#34;&gt;About NordVPN&lt;/h4&gt;
&lt;p&gt;I never talked to NordVPN, but after trialing 4-5 different VPN services that were recommended for privacy, I ended up at Nord and am very happy with them (dark patterns like difficulty to unsubscribe aside). The speed is very consistent and it&amp;rsquo;s one of the only options I was really able to keep turned on 24h a day.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re thinking of signing up for NordVPN, consider using &lt;a href=&#34;https://ref.nordvpn.com/pSuUglACVGO&#34;&gt;my referal&lt;/a&gt;&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>These days I&rsquo;ve been trying to be more conscious about my online footprint and privacy in general. I&rsquo;ve been using one-off email addresses for years now (btw check out my recent post on <a href="https://david.coffee/creating-fastmail-masked-emails-without-1password-anytime-and-everywhere">masked emails</a> if you haven&rsquo;t yet), and have a bunch of little scripts that spit out things like random usernames when I hit &ldquo;!&rdquo; 3 times.</p>
<p>But honestly speaking, IP addresses were one of those things I just kind of didn&rsquo;t really care about.</p>
<p>What changed my view drastically was when I hacked on a <a href="https://workers.cloudflare.com/">cloudflare workers</a> project and for debug purpose decided to <code>console.log()</code> the Request object. Here&rsquo;s what got printed</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;message&#34;</span>: [
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;cf&#34;</span>: {
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;longitude&#34;</span>: <span style="color:#e6db74">&#34;REDACTED&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;latitude&#34;</span>: <span style="color:#e6db74">&#34;REDACTED&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;continent&#34;</span>: <span style="color:#e6db74">&#34;AS&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;country&#34;</span>: <span style="color:#e6db74">&#34;JP&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;tlsVersion&#34;</span>: <span style="color:#e6db74">&#34;TLSv1.3&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;colo&#34;</span>: <span style="color:#e6db74">&#34;NRT&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;timezone&#34;</span>: <span style="color:#e6db74">&#34;Asia/Tokyo&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;city&#34;</span>: <span style="color:#e6db74">&#34;REDACTED&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;edgeRequestKeepAliveStatus&#34;</span>: <span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;requestPriority&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;httpProtocol&#34;</span>: <span style="color:#e6db74">&#34;HTTP/3&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;region&#34;</span>: <span style="color:#e6db74">&#34;Tokyo&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;regionCode&#34;</span>: <span style="color:#e6db74">&#34;13&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;asOrganization&#34;</span>: <span style="color:#e6db74">&#34;REDACTED&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;postalCode&#34;</span>: <span style="color:#e6db74">&#34;REDACTED&#34;</span>
</span></span><span style="display:flex;"><span>          },
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      ],
</span></span><span style="display:flex;"><span>    }
</span></span></code></pre></div><p>I cut out some of the details here, but the request had longitude, latitude, my internet provider name and a postal code that is <em>very</em> close to mine. Meaning <em>every site</em> I interact with basically knows where I live to a certain degree. That ain&rsquo;t good.</p>
<p>I now have a VPN running almost all of the time, which brings me to the main topic of this post:</p>
<p>When using a VPN, like NordVPN, and you want to let that run 24/7 so that you can move all traffic without leaks through the it, you&rsquo;ll sooner or later end up running into issues with things like local network no longer resolving because it&rsquo;s not part of the virtual network.</p>
<p>How do you fix that?</p>
<h2 id="temporary-rules-with-route">Temporary rules with route</h2>
<p>Mac has a bunch of neat network tools that you can use to get around this, the easiest is <code>route</code>:</p>
<pre tabindex="0"><code># route &lt;target&gt; &lt;gateway&gt;
sudo route -nv add -net 10.0 192.168.1.1
</code></pre><p>with the <code>-net</code> flag (there is also <code>-host</code>), <code>10.0</code> expands to <code>10.0.0.0</code>, so the entire subnet of <code>10.0.*</code> will get routed to the gateway <code>192.168.1.1</code> instead of the VPN network, sweet! (10.0.5.* is my network for services and smaller devices)</p>
<p>Routes added with <code>route</code> are temporary, so once you restart they&rsquo;re gone which makes them good for experimenting and quickly setting things up in a non-destructive way.</p>
<h2 id="making-things-persist">Making things persist</h2>
<p>For persistent rules, mac comes with the <code>networksetup</code> tool:</p>
<pre tabindex="0"><code>❯ networksetup -listallnetworkservices

An asterisk (*) denotes that a network service is disabled.
Ethernet
USB 10/100/1000 LAN
Wi-Fi
Thunderbolt Bridge
NordVPN NordLynx
</code></pre><p>The command we want is <code>-setadditionalroutes</code>. This command takes a interface, and then triplets of <code>&lt;target&gt; &lt;netmask&gt; &lt;gateway&gt;</code></p>
<pre tabindex="0"><code>networksetup -setadditionalroutes &#34;Ethernet&#34; 10.0.5.0 255.255.255.0 192.168.1.1
</code></pre><p>This command adds a route for <code>10.0.5.*</code> (designated by the netmask <code>255.255.255.0</code>) and routes all traffic to the gateway <code>192.168.1.1</code> instead.</p>
<p>Repeat that for all your interfaces that you use to connect, and done.</p>
<p>To add multiple routes you&rsquo;ll have to specify them in the same go, in triplets:</p>
<pre tabindex="0"><code>networksetup -setadditionalroutes &#34;Ethernet&#34; 192.168.0.0 255.255.0.0 192.168.1.1 10.0.5.0 255.255.255.0 192.168.1.1
</code></pre><p>Now everything from <code>192.168.*</code> and <code>10.0.5.*</code> will get routed to my router</p>
<h4 id="about-nordvpn">About NordVPN</h4>
<p>I never talked to NordVPN, but after trialing 4-5 different VPN services that were recommended for privacy, I ended up at Nord and am very happy with them (dark patterns like difficulty to unsubscribe aside). The speed is very consistent and it&rsquo;s one of the only options I was really able to keep turned on 24h a day.</p>
<p>If you&rsquo;re thinking of signing up for NordVPN, consider using <a href="https://ref.nordvpn.com/pSuUglACVGO">my referal</a></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Creating Fastmail Masked Emails without 1Password anytime and everywhere</title>
      <link>https://david.coffee/creating-fastmail-masked-emails-without-1password-anytime-and-everywhere/</link>
      <pubDate>Fri, 17 Dec 2021 15:39:23 +0000</pubDate>
      
      <guid>https://david.coffee/creating-fastmail-masked-emails-without-1password-anytime-and-everywhere/</guid><description>&lt;p&gt;I love Masked Emails, Fastmails take on one-time-use email addresses. I&amp;rsquo;ve been doing something similar myself for years now, but it is a bit of a shame that it&amp;rsquo;s so tightly coupled to 1Password. Sure you &lt;em&gt;can&lt;/em&gt; create them through the webinterface now, but it&amp;rsquo;s still kind of a pain to use and not very convenient (unless you use 1Password).&lt;/p&gt;
&lt;p&gt;You can&amp;rsquo;t even manually trigger creation of one within 1Password, so when the extension doesn&amp;rsquo;t detect an email signup field, you&amp;rsquo;re sorta out of luck.&lt;/p&gt;
&lt;p&gt;What I want is something like this:&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./masked-email-showcase.gif&#34; alt=&#34;showcase&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s get it working.&lt;/p&gt;
&lt;p&gt;The first thing I noticed is that the request to create a new masked email (through 1Password) isn&amp;rsquo;t actually coming from the 1Password servers like I first thought, it goes directly from the client to api.fastmail.com, like seen on a reverse-proxy like Proxyman:&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./masked-email-proxyman.jpg&#34; alt=&#34;Network screenshot&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;By the way, Fastmail also stated they put this functionality right into their open-source JMAP project &lt;a href=&#34;https://blog.1password.com/making-masked-email-with-jmap/&#34;&gt;in a blogpost&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After a bit of reverse-engineering the request format to the API server and replicating it in Golang:&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./masked-email-request.jpg&#34; alt=&#34;Requests&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;I present you: &lt;a href=&#34;https://github.com/dvcrn/maskedemail-cli&#34;&gt;https://github.com/dvcrn/maskedemail-cli&lt;/a&gt;&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;maskedemail-cli -accountid xxxx -token abcdef12345 create facebook.com
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&amp;hellip; and you get a new masked email for facebook.com, right in your shell!&lt;/p&gt;
&lt;h2 id=&#34;glueing-the-cli-to-shortcuts-with-keyboardmaestro&#34;&gt;Glueing the CLI to shortcuts with KeyboardMaestro&lt;/h2&gt;
&lt;p&gt;I wanted this to be available everywhere whenever I need to. A full extension for a product that isn&amp;rsquo;t mine would have been too much work, and I wanted it to work outside browsers as well. With something like KeyboardMaestro this is trivial to do&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the macro from above:&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./masked-email-keyboardmaestro.png&#34; alt=&#34;KeyboardMaestro Macro&#34; /&gt;

&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Insert &amp;ldquo;&amp;hellip;&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Retrieve the URL from the current browser window. I had to use some glue applescript here because Firefox has no scripting interface&lt;/li&gt;
&lt;li&gt;Pipe it into the maskedemail-cli&lt;/li&gt;
&lt;li&gt;Remove the &amp;ldquo;&amp;hellip;&amp;rdquo; and paste the newly created email&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;other-thoughts&#34;&gt;Other thoughts&lt;/h2&gt;
&lt;p&gt;I am sure this will become more available in the near future. Fastmail likely has some kind of exclusivity thing with 1Password for now, but it&amp;rsquo;s great to see that we can hack ontop of it so easily. It&amp;rsquo;s also very cool that this is built right into JMAP. I hope we&amp;rsquo;ll see more email providers implementing it!&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>I love Masked Emails, Fastmails take on one-time-use email addresses. I&rsquo;ve been doing something similar myself for years now, but it is a bit of a shame that it&rsquo;s so tightly coupled to 1Password. Sure you <em>can</em> create them through the webinterface now, but it&rsquo;s still kind of a pain to use and not very convenient (unless you use 1Password).</p>
<p>You can&rsquo;t even manually trigger creation of one within 1Password, so when the extension doesn&rsquo;t detect an email signup field, you&rsquo;re sorta out of luck.</p>
<p>What I want is something like this:</p>
<p>
<img class="img-fluid" src="./masked-email-showcase.gif" alt="showcase" />

</p>
<p>Let&rsquo;s get it working.</p>
<p>The first thing I noticed is that the request to create a new masked email (through 1Password) isn&rsquo;t actually coming from the 1Password servers like I first thought, it goes directly from the client to api.fastmail.com, like seen on a reverse-proxy like Proxyman:</p>
<p>
<img class="img-fluid" src="./masked-email-proxyman.jpg" alt="Network screenshot" />

</p>
<p>By the way, Fastmail also stated they put this functionality right into their open-source JMAP project <a href="https://blog.1password.com/making-masked-email-with-jmap/">in a blogpost</a>.</p>
<p>After a bit of reverse-engineering the request format to the API server and replicating it in Golang:</p>
<p>
<img class="img-fluid" src="./masked-email-request.jpg" alt="Requests" />

</p>
<p>I present you: <a href="https://github.com/dvcrn/maskedemail-cli">https://github.com/dvcrn/maskedemail-cli</a></p>
<pre tabindex="0"><code>maskedemail-cli -accountid xxxx -token abcdef12345 create facebook.com
</code></pre><p>&hellip; and you get a new masked email for facebook.com, right in your shell!</p>
<h2 id="glueing-the-cli-to-shortcuts-with-keyboardmaestro">Glueing the CLI to shortcuts with KeyboardMaestro</h2>
<p>I wanted this to be available everywhere whenever I need to. A full extension for a product that isn&rsquo;t mine would have been too much work, and I wanted it to work outside browsers as well. With something like KeyboardMaestro this is trivial to do</p>
<p>Here&rsquo;s the macro from above:</p>
<p>
<img class="img-fluid" src="./masked-email-keyboardmaestro.png" alt="KeyboardMaestro Macro" />

</p>
<ol>
<li>Insert &ldquo;&hellip;&rdquo;</li>
<li>Retrieve the URL from the current browser window. I had to use some glue applescript here because Firefox has no scripting interface</li>
<li>Pipe it into the maskedemail-cli</li>
<li>Remove the &ldquo;&hellip;&rdquo; and paste the newly created email</li>
</ol>
<h2 id="other-thoughts">Other thoughts</h2>
<p>I am sure this will become more available in the near future. Fastmail likely has some kind of exclusivity thing with 1Password for now, but it&rsquo;s great to see that we can hack ontop of it so easily. It&rsquo;s also very cool that this is built right into JMAP. I hope we&rsquo;ll see more email providers implementing it!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Thoughts on Metaplex, candymachine and NFTs on Solana</title>
      <link>https://david.coffee/thoughts-on-metaplex-candymachine-and-nfts-on-solana/</link>
      <pubDate>Fri, 10 Dec 2021 11:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/thoughts-on-metaplex-candymachine-and-nfts-on-solana/</guid><description>&lt;p&gt;I have a few comments on metaplex, the batteries-included go-to solution for everything NFT related out-of-the-box, that I wanted to share with the world.&lt;/p&gt;
&lt;p&gt;These are mostly thoughts I had when dealing with metaplex during the lifetime of a few projects, from the perspective of someone who just joined the Solana ecosystem.&lt;/p&gt;
&lt;p&gt;About myself: I worked on a few production projects on Solana, am sometimes active on the Solana discord &lt;code&gt;#developer-support&lt;/code&gt; channel to help newcomers out, and currently work on other (NFT related) projects. The following points are my thoughts and experience from building projects in this space.&lt;/p&gt;
&lt;h2 id=&#34;instructions-often-unclear&#34;&gt;Instructions (often) unclear&lt;/h2&gt;
&lt;p&gt;When you arrive in Solana land and want to do anything NFT related, chances are that you end up on the metaplex GitHub repo, or documentation at &lt;a href=&#34;https://docs.metaplex.com/&#34;&gt;https://docs.metaplex.com/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Coming from Ethereum, we have the ERC721 standard that dictates a handful of functions a smart contract has to implement to be considered ERC721 compliant.&lt;/p&gt;
&lt;p&gt;In solana there are no smart contracts like in Ethereum, instead we&amp;rsquo;re dealing with &lt;em&gt;pRoGRaMs&lt;/em&gt;. Tokens are also not handled by an individual ERC20/ERC721 contract per token, but by the central spl-token program. In fact, each token on Solana is owned by the spl-token program (spl stands for Solana Program Library).&lt;/p&gt;
&lt;p&gt;After clicking around you might land on &lt;a href=&#34;https://docs.metaplex.com/nft-standard&#34;&gt;this page&lt;/a&gt; describing a metadata standard on Solana. Okay so far so good, but how do you actually use it? What is the address of this metadata program that you&amp;rsquo;re supposed to use? How does the function call look like to register metadata to something? Don&amp;rsquo;t know.&lt;/p&gt;
&lt;p&gt;There is a metadata overview docs page available at &lt;a href=&#34;https://docs.metaplex.com/overviews/metadata_overview&#34;&gt;https://docs.metaplex.com/overviews/metadata_overview&lt;/a&gt; but at the time of writing is still empty.&lt;/p&gt;
&lt;p&gt;After a bit more of digging and wrapping my head around it, I found the metadata program address defined in the &lt;a href=&#34;https://github.com/metaplex-foundation/metaplex/blob/master/rust/token-metadata/program/src/lib.rs#L15&#34;&gt;lib.rs&lt;/a&gt; file of the program itself:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; 	solana_program::&lt;span style=&#34;color:#a6e22e&#34;&gt;declare_id!&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Why is such a crucial bit of information not easier to find?&lt;/p&gt;
&lt;p&gt;Then there is a single &lt;a href=&#34;https://crates.io/crates/spl-token-metadata&#34;&gt;spl-token-metadata&lt;/a&gt; crate available on crates.io that you might want to use to submit metadata from your onchain rust programs, but after banging my head against it for literal hours and trying to get it working, it turned out this isn&amp;rsquo;t an official package and &lt;a href=&#34;https://github.com/metaplex-foundation/metaplex/issues/349&#34;&gt;has a bug&lt;/a&gt; that makes it impossible to submit metadata - whoops. (Thanks for the kind folks over at #developer-support that helped me debug this)&lt;/p&gt;
&lt;p&gt;The issue I have is that for something that wants to be the &lt;em&gt;standard&lt;/em&gt; of something like NFTs on the entire chain, the experience can&amp;rsquo;t be frustrating but has to be as welcoming and easy to use as possible. Right now, from a newcomers perspective, figuring out how to simply add metadata for a token is incredibly frustrating. (Nowadays there is a &lt;a href=&#34;https://github.com/metaplex-foundation/js/blob/main/src/actions/createMetadata.ts#L18&#34;&gt;JS SDK&lt;/a&gt; available that has functions to create metadata, but try to figure that out without initial guidance)&lt;/p&gt;
&lt;p&gt;When dealing with other metaplex components such as candymachine (a program to sell mints of a NFT collection) we are running into a similar situation - the official documentation available &lt;a href=&#34;https://docs.metaplex.com/create-candy/introduction&#34;&gt;here&lt;/a&gt; is missing half it&amp;rsquo;s pages* and instead of giving a technical overview of how it works, relies on commands to copy. If something isn&amp;rsquo;t working, which it often doesn&amp;rsquo;t, people flock into the discord to ask questions because they aren&amp;rsquo;t able to debug it themselves.&lt;/p&gt;
&lt;p&gt;Instead of official documentation guidance, people relied on blog posts by other people that explain how the candymachine is working and how to set one up properly.&lt;/p&gt;
&lt;p&gt;*/EDIT: in the time it took me to finish this post, the documentation has been updated and has more pages now. Thanks!&lt;/p&gt;
&lt;p&gt;That brings me to my next point&lt;/p&gt;
&lt;h2 id=&#34;copy--pasting-encouraged-over-thinking-and-understanding&#34;&gt;Copy &amp;amp; Pasting encouraged over thinking and understanding&lt;/h2&gt;
&lt;p&gt;While dealing with metaplex, be it the metadata program, candymachine or other components, it became very apparent to me that the creators of metaplex had envisioned one very specific way of dealing with their projects.&lt;/p&gt;
&lt;p&gt;Candymachine, fair-launch and other tools have a very specific way to set them up that they want users to copy and paste, rather than trying to understand and reason with. Of course being friendly to non-technical people is good, but this doesn&amp;rsquo;t teach users (technical and non-technical alike) how things are actually working, but instead encourages to stop thinking and follow guides. When technical people (like myself) try to understand and reason with the programs, we run into more road blocks due to lack of documentation, code quality and resources available.&lt;/p&gt;
&lt;p&gt;In tech, copy-pasting and using big blobs of programs without attempts to understand them is usually a bad idea, especially when money flow and your business depends on it. If anything breaks you need to be able to reason with the program and figure out what is wrong.&lt;/p&gt;
&lt;p&gt;When using metaplex like the creators intended to, so following candymachine instructions step by step with the provided CLI, and submit NFT metadata through candymachine itself, then everything works fine (unless you run any of the small issue resulting in one of the many crypto errors that have no reply on github). But when you try to use them individually, such as only using the metadata program without any of the master edition stuff, or candymachine components without the CLI, things can get fuzzy. (I still don&amp;rsquo;t know if it&amp;rsquo;s okay to remove the minting authority from a token that needs metadata - &lt;a href=&#34;https://github.com/metaplex-foundation/metaplex/issues/279&#34;&gt;https://github.com/metaplex-foundation/metaplex/issues/279&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;One example is IPFS. Candymachine supports IPFS, but only through Infura instead of allowing to actually use IPFS p2p. Without paying for Infura it is not even possible to create a candymachine with 10 mints without running into rate limitations and constant retries (I had to find the code and add a blocking &lt;code&gt;sleep&lt;/code&gt; for things to go smooth). Of course when using arweave, like the creators wanted it to be used with, everything works fine. It&amp;rsquo;s also what 80% of the candymachine projects use because the instructions use that as default, often without understanding what arweave actually is.&lt;/p&gt;
&lt;p&gt;Of course biased tools are completely fine, but my problem is more the messaging that everyone in Solana is supposed to be using this otherwise you&amp;rsquo;ll have a &amp;ldquo;&lt;em&gt;bad time&lt;/em&gt;&amp;rdquo;. There are occasionally even people that don&amp;rsquo;t want to deal with you if you created your own minting onchain program and don&amp;rsquo;t use candymachine.&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s a shame because&amp;hellip;&lt;/p&gt;
&lt;h2 id=&#34;candymachine-is-centralized-and-heavily-botted&#34;&gt;Candymachine is centralized and heavily botted&lt;/h2&gt;
&lt;p&gt;I mentioned above that in Solana, the spl-token program is kind of a central program (like the system program) that handles one thing - tokens.&lt;/p&gt;
&lt;p&gt;For a system program I think that&amp;rsquo;s fair, but candymachine (and metadata but that&amp;rsquo;s not that big of an issue) are following the same pattern and trying to be the centralized thing that your application or dApp is talking to. Imagine it a little bit like having a &amp;ldquo;NFT sale server&amp;rdquo; and to buy an NFT you make a request like this (imaginary example):&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;POST candymachine.com/buy
{
	candymachineID: &amp;#34;abc123&amp;#34;,
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The candymachine program manages &lt;em&gt;all&lt;/em&gt; candymachines. This program can also be upgraded at will (upgrade authority has not been revoked - &lt;a href=&#34;https://explorer.solana.com/address/cndyAnrLdpjq1Ssp1z8xxDsB8dxe7u4HL5Nxi2K5WXZ&#34;&gt;https://explorer.solana.com/address/cndyAnrLdpjq1Ssp1z8xxDsB8dxe7u4HL5Nxi2K5WXZ&lt;/a&gt; by it&amp;rsquo;s creators and functionality can be changed without prior notice).&lt;/p&gt;
&lt;p&gt;Being a central program, it is very trivial to see the transactions from other projects to setup a new NFT mint. It usually goes like this (pseudocode):&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;POST candymachine/setup-machine
POST candymachine/add-mint project1 1 &amp;#34;My Mint #1&amp;#34;
POST candymachine/add-mint project2 2 &amp;#34;Another Mint #2&amp;#34;
POST candymachine/add-mint project2 5 &amp;#34;Another Mint #5&amp;#34;
...
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once you monitor the onchain program you can &lt;em&gt;very easily&lt;/em&gt; extract all available NFTs including their mint order from any project that uses candymachine. This sparked a huge amount of botters that take advantage of this, to either snipe rare NFTs, buy-out huge amounts of NFTs or in some cases even accidentally buying &lt;em&gt;all&lt;/em&gt; available NFTs.&lt;/p&gt;
&lt;p&gt;In other words: If you are a NFT buyer on Solana, you have &lt;strong&gt;almost no chance to mint a rare one&lt;/strong&gt;. Bots know ahead of time if a mint is rare, and will be faster than you buying it.&lt;/p&gt;
&lt;p&gt;To combat this, projects are now creating &lt;em&gt;multiple candymachines&lt;/em&gt; with some fake ones, just to confuse botters.&lt;/p&gt;
&lt;p&gt;If, however, candymachine was setup to be a program that you fork, adjust and deploy yourself, it would be a different story because botters would be unable to figure the program address ahead of time and thus can&amp;rsquo;t bot it (let alone knowing what input is needed to submit a mint transaction).&lt;/p&gt;
&lt;p&gt;I see no reason that candymachine should be a central program. A strength of blockchains is decentralization so let&amp;rsquo;s take more advantage of it instead of relying on central entities that aren&amp;rsquo;t immutable.&lt;/p&gt;
&lt;h2 id=&#34;wrapping-up&#34;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;I sound critical and grumpy, but it&amp;rsquo;s good that there is so much NFT adoption happening on Solana. Candymachine being this easy to setup is certainly a reason why we have a huuuge amount of new projects popping up every day (although some with very low quality).&lt;/p&gt;
&lt;p&gt;I hope the Solana foundation can pickup the Metadata standard or make an official one, and I hope that &lt;a href=&#34;https://github.com/metaplex-foundation/metaplex/tree/master/rust/nft-candy-machine-v2&#34;&gt;candymachinev2&lt;/a&gt; will solve some of the issues I mentioned above. Or even better: Make something slim that people can fork, modify and deploy themselves, that would be nice!&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>I have a few comments on metaplex, the batteries-included go-to solution for everything NFT related out-of-the-box, that I wanted to share with the world.</p>
<p>These are mostly thoughts I had when dealing with metaplex during the lifetime of a few projects, from the perspective of someone who just joined the Solana ecosystem.</p>
<p>About myself: I worked on a few production projects on Solana, am sometimes active on the Solana discord <code>#developer-support</code> channel to help newcomers out, and currently work on other (NFT related) projects. The following points are my thoughts and experience from building projects in this space.</p>
<h2 id="instructions-often-unclear">Instructions (often) unclear</h2>
<p>When you arrive in Solana land and want to do anything NFT related, chances are that you end up on the metaplex GitHub repo, or documentation at <a href="https://docs.metaplex.com/">https://docs.metaplex.com/</a>.</p>
<p>Coming from Ethereum, we have the ERC721 standard that dictates a handful of functions a smart contract has to implement to be considered ERC721 compliant.</p>
<p>In solana there are no smart contracts like in Ethereum, instead we&rsquo;re dealing with <em>pRoGRaMs</em>. Tokens are also not handled by an individual ERC20/ERC721 contract per token, but by the central spl-token program. In fact, each token on Solana is owned by the spl-token program (spl stands for Solana Program Library).</p>
<p>After clicking around you might land on <a href="https://docs.metaplex.com/nft-standard">this page</a> describing a metadata standard on Solana. Okay so far so good, but how do you actually use it? What is the address of this metadata program that you&rsquo;re supposed to use? How does the function call look like to register metadata to something? Don&rsquo;t know.</p>
<p>There is a metadata overview docs page available at <a href="https://docs.metaplex.com/overviews/metadata_overview">https://docs.metaplex.com/overviews/metadata_overview</a> but at the time of writing is still empty.</p>
<p>After a bit more of digging and wrapping my head around it, I found the metadata program address defined in the <a href="https://github.com/metaplex-foundation/metaplex/blob/master/rust/token-metadata/program/src/lib.rs#L15">lib.rs</a> file of the program itself:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span> 	solana_program::<span style="color:#a6e22e">declare_id!</span>(<span style="color:#e6db74">&#34;metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s&#34;</span>);
</span></span></code></pre></div><p>Why is such a crucial bit of information not easier to find?</p>
<p>Then there is a single <a href="https://crates.io/crates/spl-token-metadata">spl-token-metadata</a> crate available on crates.io that you might want to use to submit metadata from your onchain rust programs, but after banging my head against it for literal hours and trying to get it working, it turned out this isn&rsquo;t an official package and <a href="https://github.com/metaplex-foundation/metaplex/issues/349">has a bug</a> that makes it impossible to submit metadata - whoops. (Thanks for the kind folks over at #developer-support that helped me debug this)</p>
<p>The issue I have is that for something that wants to be the <em>standard</em> of something like NFTs on the entire chain, the experience can&rsquo;t be frustrating but has to be as welcoming and easy to use as possible. Right now, from a newcomers perspective, figuring out how to simply add metadata for a token is incredibly frustrating. (Nowadays there is a <a href="https://github.com/metaplex-foundation/js/blob/main/src/actions/createMetadata.ts#L18">JS SDK</a> available that has functions to create metadata, but try to figure that out without initial guidance)</p>
<p>When dealing with other metaplex components such as candymachine (a program to sell mints of a NFT collection) we are running into a similar situation - the official documentation available <a href="https://docs.metaplex.com/create-candy/introduction">here</a> is missing half it&rsquo;s pages* and instead of giving a technical overview of how it works, relies on commands to copy. If something isn&rsquo;t working, which it often doesn&rsquo;t, people flock into the discord to ask questions because they aren&rsquo;t able to debug it themselves.</p>
<p>Instead of official documentation guidance, people relied on blog posts by other people that explain how the candymachine is working and how to set one up properly.</p>
<p>*/EDIT: in the time it took me to finish this post, the documentation has been updated and has more pages now. Thanks!</p>
<p>That brings me to my next point</p>
<h2 id="copy--pasting-encouraged-over-thinking-and-understanding">Copy &amp; Pasting encouraged over thinking and understanding</h2>
<p>While dealing with metaplex, be it the metadata program, candymachine or other components, it became very apparent to me that the creators of metaplex had envisioned one very specific way of dealing with their projects.</p>
<p>Candymachine, fair-launch and other tools have a very specific way to set them up that they want users to copy and paste, rather than trying to understand and reason with. Of course being friendly to non-technical people is good, but this doesn&rsquo;t teach users (technical and non-technical alike) how things are actually working, but instead encourages to stop thinking and follow guides. When technical people (like myself) try to understand and reason with the programs, we run into more road blocks due to lack of documentation, code quality and resources available.</p>
<p>In tech, copy-pasting and using big blobs of programs without attempts to understand them is usually a bad idea, especially when money flow and your business depends on it. If anything breaks you need to be able to reason with the program and figure out what is wrong.</p>
<p>When using metaplex like the creators intended to, so following candymachine instructions step by step with the provided CLI, and submit NFT metadata through candymachine itself, then everything works fine (unless you run any of the small issue resulting in one of the many crypto errors that have no reply on github). But when you try to use them individually, such as only using the metadata program without any of the master edition stuff, or candymachine components without the CLI, things can get fuzzy. (I still don&rsquo;t know if it&rsquo;s okay to remove the minting authority from a token that needs metadata - <a href="https://github.com/metaplex-foundation/metaplex/issues/279">https://github.com/metaplex-foundation/metaplex/issues/279</a>)</p>
<p>One example is IPFS. Candymachine supports IPFS, but only through Infura instead of allowing to actually use IPFS p2p. Without paying for Infura it is not even possible to create a candymachine with 10 mints without running into rate limitations and constant retries (I had to find the code and add a blocking <code>sleep</code> for things to go smooth). Of course when using arweave, like the creators wanted it to be used with, everything works fine. It&rsquo;s also what 80% of the candymachine projects use because the instructions use that as default, often without understanding what arweave actually is.</p>
<p>Of course biased tools are completely fine, but my problem is more the messaging that everyone in Solana is supposed to be using this otherwise you&rsquo;ll have a &ldquo;<em>bad time</em>&rdquo;. There are occasionally even people that don&rsquo;t want to deal with you if you created your own minting onchain program and don&rsquo;t use candymachine.</p>
<p>And that&rsquo;s a shame because&hellip;</p>
<h2 id="candymachine-is-centralized-and-heavily-botted">Candymachine is centralized and heavily botted</h2>
<p>I mentioned above that in Solana, the spl-token program is kind of a central program (like the system program) that handles one thing - tokens.</p>
<p>For a system program I think that&rsquo;s fair, but candymachine (and metadata but that&rsquo;s not that big of an issue) are following the same pattern and trying to be the centralized thing that your application or dApp is talking to. Imagine it a little bit like having a &ldquo;NFT sale server&rdquo; and to buy an NFT you make a request like this (imaginary example):</p>
<pre tabindex="0"><code>POST candymachine.com/buy
{
	candymachineID: &#34;abc123&#34;,
}
</code></pre><p>The candymachine program manages <em>all</em> candymachines. This program can also be upgraded at will (upgrade authority has not been revoked - <a href="https://explorer.solana.com/address/cndyAnrLdpjq1Ssp1z8xxDsB8dxe7u4HL5Nxi2K5WXZ">https://explorer.solana.com/address/cndyAnrLdpjq1Ssp1z8xxDsB8dxe7u4HL5Nxi2K5WXZ</a> by it&rsquo;s creators and functionality can be changed without prior notice).</p>
<p>Being a central program, it is very trivial to see the transactions from other projects to setup a new NFT mint. It usually goes like this (pseudocode):</p>
<pre tabindex="0"><code>POST candymachine/setup-machine
POST candymachine/add-mint project1 1 &#34;My Mint #1&#34;
POST candymachine/add-mint project2 2 &#34;Another Mint #2&#34;
POST candymachine/add-mint project2 5 &#34;Another Mint #5&#34;
...
</code></pre><p>Once you monitor the onchain program you can <em>very easily</em> extract all available NFTs including their mint order from any project that uses candymachine. This sparked a huge amount of botters that take advantage of this, to either snipe rare NFTs, buy-out huge amounts of NFTs or in some cases even accidentally buying <em>all</em> available NFTs.</p>
<p>In other words: If you are a NFT buyer on Solana, you have <strong>almost no chance to mint a rare one</strong>. Bots know ahead of time if a mint is rare, and will be faster than you buying it.</p>
<p>To combat this, projects are now creating <em>multiple candymachines</em> with some fake ones, just to confuse botters.</p>
<p>If, however, candymachine was setup to be a program that you fork, adjust and deploy yourself, it would be a different story because botters would be unable to figure the program address ahead of time and thus can&rsquo;t bot it (let alone knowing what input is needed to submit a mint transaction).</p>
<p>I see no reason that candymachine should be a central program. A strength of blockchains is decentralization so let&rsquo;s take more advantage of it instead of relying on central entities that aren&rsquo;t immutable.</p>
<h2 id="wrapping-up">Wrapping up</h2>
<p>I sound critical and grumpy, but it&rsquo;s good that there is so much NFT adoption happening on Solana. Candymachine being this easy to setup is certainly a reason why we have a huuuge amount of new projects popping up every day (although some with very low quality).</p>
<p>I hope the Solana foundation can pickup the Metadata standard or make an official one, and I hope that <a href="https://github.com/metaplex-foundation/metaplex/tree/master/rust/nft-candy-machine-v2">candymachinev2</a> will solve some of the issues I mentioned above. Or even better: Make something slim that people can fork, modify and deploy themselves, that would be nice!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Getting SoftBank WiFi hotspots working on Mac</title>
      <link>https://david.coffee/getting-softbank-wifi-hotspots-working-on-mac/</link>
      <pubDate>Thu, 08 Dec 2016 01:59:00 +0000</pubDate>
      
      <guid>https://david.coffee/getting-softbank-wifi-hotspots-working-on-mac/</guid><description>&lt;p&gt;&lt;em&gt;This was originally posted &lt;a href=&#34;https:/..com/@davidmohl/getting-softbank-wifi-hotspots-working-on-mac-b1f31a73cf37&#34;&gt;on my medium&lt;/a&gt; but has been backported into this blog.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Here in Japan, every phone provider gives you access to their WiFi Hotspots when you sign up. They are great! They cover areas where your LTE might get a bit wonky and make sure you’re not eating all of your data right away.&lt;/p&gt;
&lt;p&gt;Sadly though if you’ve ever been to Tokyo, the free WiFi spots that are available here (be it in Starbucks or other cafes) are… just not very good. Most of them are powered by &lt;a href=&#34;http://wi2.co.jp/jp/&#34;&gt;Wi2&lt;/a&gt; and while not necessarily bad, I never saw this many disconnects with any other provider. You constantly have to re-connect to the hotspot because all of a sudden everything stops working. Most of the time when you’re in the middle of something very important like browsing cat pictures or finishing that last 1% of a download.&lt;/p&gt;
&lt;p&gt;Wi2 also offers a dirt-cheap WiFi plan that costs 400 yen ($4) a month and has pretty good coverage given what you pay for it. It’s my go-to recommendation when someone asks me which service to use, though it suffers from the same problems mentioned above.&lt;/p&gt;
&lt;p&gt;Alternatives are the docomo tourist WiFi or some other services that charge you per hour, but &lt;strong&gt;why do I have to pay more when I already pay for my phone contract which includes SoftBank WiFi?&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&#34;testing-waters&#34;&gt;Testing waters&lt;/h4&gt;
&lt;p&gt;Just connecting to SoftBank WiFi on my Mac is giving me a error message. Who would have expected that.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./1_err.jpeg&#34; alt=&#34;&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;“Aha!” he thought, “they are just filtering by User Agent!”. Open is the developer menu in Safari and the User Agent switched to SoftBank and sure enough, the message changed! Now telling me to go to “sbwifi.jp”. Though when trying to navigate it…&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./1__F3__kBZta6__z1eeYMbcRvdA.png&#34; alt=&#34;&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;didn’t work.&lt;/p&gt;
&lt;p&gt;When connecting with my phone to the same WiFi hotspot, it immediately authenticated me without asking for any details. So maybe it’s using the Mac address?&lt;/p&gt;
&lt;p&gt;I authenticated on the WiFi hotspot, copied the Mac address to my Mac, spoofed it and disconnected my phone at roughly the same moment as my Mac starts connecting.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;sudo ifconfig eth0 hw ether 00:01:02:03:04:05
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This little trick usually gets most of the WiFi hotspots that are mobile only restricted. The phone disconnects and the Mac re-connects with the same hardware identifier the phone used. The WiFi spot thinks the phone just re-connected and all is good.&lt;/p&gt;
&lt;p&gt;But on this one… it didn’t work. Hrrmm!&lt;/p&gt;
&lt;p&gt;Something else is needed and I’m not ready yet to give up!&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./1__qkvVja6b8U5Av__qBY1RvOw.jpeg&#34; alt=&#34;&#34; /&gt;

&lt;/p&gt;
&lt;h4 id=&#34;digging-deeper&#34;&gt;Digging deeper&lt;/h4&gt;
&lt;p&gt;So, a naive filtering to show the login page happens based on User-Agent. My phone is directly authenticated. So it’s probably some sort of certificate for authentication.&lt;/p&gt;
&lt;p&gt;There is an iOS app which does authentication for you, but it only works from iOS 9 up. There must be another way.&lt;/p&gt;
&lt;p&gt;On my phone, I manually navigated to sbwifi.jp, clicked the blue button and yup, I receive a SMS with a link I should click on.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./1__qKDdqLjTTA807aBnMvK3QA.png&#34; alt=&#34;&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Clicking the link and, sure enough, it downloads a certificate file to install on my iPhone!&lt;/p&gt;
&lt;p&gt;All I have to do is get this certificate on my Mac and I should be good. I copied the link to my Mac and…! Nothing. The link only works out of the SoftBank network and not through WiFi. Dang it.&lt;/p&gt;
&lt;h4 id=&#34;the-lastmile&#34;&gt;The last mile&lt;/h4&gt;
&lt;p&gt;What we got so far:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;SoftBank uses UA to show you the correct login page&lt;/li&gt;
&lt;li&gt;Not authenticated users get a “please go to sbwifi.jp” page&lt;/li&gt;
&lt;li&gt;sbwifi.jp only works out of the SoftBank LTE/3G network and sends a link to a profile through SMS to the users phone&lt;/li&gt;
&lt;li&gt;The link to the profile, again, only works out of the SoftBank LTE/3G network&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So how do I get the profile on my Mac? The iPhone doesn’t let me download and share it through email and my Mac can’t download it. Just doesn’t work.&lt;/p&gt;
&lt;p&gt;Luckily though, nowadays we can just enter the SoftBank network on our Mac! How? Through our phone of course!&lt;/p&gt;
&lt;p&gt;To enable tethering on SoftBank iPhones you have to ‘purchase’ the tethering option through &lt;a href=&#34;http://my.softbank.jp&#34;&gt;my.softbank.jp&lt;/a&gt;. Currently though that’s free for 2 years, so just enable it, wait a little bit and the “Personal Hotspot” option pops up in the settings.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./1__WIP__dU6dVu8YxVWLah6LJw.png&#34; alt=&#34;&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Creating a hotspot… connecting to it… and….&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./1__TztlUjmIz2fk3PfBGA7x4w.png&#34; alt=&#34;&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Let’s try it again. UA switched to ‘iPhone’, navigating to sbwifi.jp, clicking the button and… yup there it is, the SMS to the profile. Copied to the Mac, clicked on it&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./1__2UwDS6KaQca5hnQjMJNfzg.png&#34; alt=&#34;&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;jackpot.&lt;/p&gt;
&lt;p&gt;Luckily macOS is compatible with iPhone configuration profiles out of the box so we can go ahead and natively install it without any issues.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./1__yr__YSchiNrRKvciUzd22jg.png&#34; alt=&#34;&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Now all we have to do is disconnect from our iPhone, make sure the iPhone doesn’t automatically connect to SoftBank Wifi at the same time, connect to 0001softbank and tada! We have blazing fast internet :)&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./1__6__2qC__EnNt__EUrXjimDr4A.gif&#34; alt=&#34;&#34; /&gt;

&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p><em>This was originally posted <a href="https:/..com/@davidmohl/getting-softbank-wifi-hotspots-working-on-mac-b1f31a73cf37">on my medium</a> but has been backported into this blog.</em></p>
<p>Here in Japan, every phone provider gives you access to their WiFi Hotspots when you sign up. They are great! They cover areas where your LTE might get a bit wonky and make sure you’re not eating all of your data right away.</p>
<p>Sadly though if you’ve ever been to Tokyo, the free WiFi spots that are available here (be it in Starbucks or other cafes) are… just not very good. Most of them are powered by <a href="http://wi2.co.jp/jp/">Wi2</a> and while not necessarily bad, I never saw this many disconnects with any other provider. You constantly have to re-connect to the hotspot because all of a sudden everything stops working. Most of the time when you’re in the middle of something very important like browsing cat pictures or finishing that last 1% of a download.</p>
<p>Wi2 also offers a dirt-cheap WiFi plan that costs 400 yen ($4) a month and has pretty good coverage given what you pay for it. It’s my go-to recommendation when someone asks me which service to use, though it suffers from the same problems mentioned above.</p>
<p>Alternatives are the docomo tourist WiFi or some other services that charge you per hour, but <strong>why do I have to pay more when I already pay for my phone contract which includes SoftBank WiFi?</strong></p>
<h4 id="testing-waters">Testing waters</h4>
<p>Just connecting to SoftBank WiFi on my Mac is giving me a error message. Who would have expected that.</p>
<p>
<img class="img-fluid" src="./1_err.jpeg" alt="" />

</p>
<p>“Aha!” he thought, “they are just filtering by User Agent!”. Open is the developer menu in Safari and the User Agent switched to SoftBank and sure enough, the message changed! Now telling me to go to “sbwifi.jp”. Though when trying to navigate it…</p>
<p>
<img class="img-fluid" src="./1__F3__kBZta6__z1eeYMbcRvdA.png" alt="" />

</p>
<p>didn’t work.</p>
<p>When connecting with my phone to the same WiFi hotspot, it immediately authenticated me without asking for any details. So maybe it’s using the Mac address?</p>
<p>I authenticated on the WiFi hotspot, copied the Mac address to my Mac, spoofed it and disconnected my phone at roughly the same moment as my Mac starts connecting.</p>
<pre tabindex="0"><code>sudo ifconfig eth0 hw ether 00:01:02:03:04:05
</code></pre><p>This little trick usually gets most of the WiFi hotspots that are mobile only restricted. The phone disconnects and the Mac re-connects with the same hardware identifier the phone used. The WiFi spot thinks the phone just re-connected and all is good.</p>
<p>But on this one… it didn’t work. Hrrmm!</p>
<p>Something else is needed and I’m not ready yet to give up!</p>
<p>
<img class="img-fluid" src="./1__qkvVja6b8U5Av__qBY1RvOw.jpeg" alt="" />

</p>
<h4 id="digging-deeper">Digging deeper</h4>
<p>So, a naive filtering to show the login page happens based on User-Agent. My phone is directly authenticated. So it’s probably some sort of certificate for authentication.</p>
<p>There is an iOS app which does authentication for you, but it only works from iOS 9 up. There must be another way.</p>
<p>On my phone, I manually navigated to sbwifi.jp, clicked the blue button and yup, I receive a SMS with a link I should click on.</p>
<p>
<img class="img-fluid" src="./1__qKDdqLjTTA807aBnMvK3QA.png" alt="" />

</p>
<p>Clicking the link and, sure enough, it downloads a certificate file to install on my iPhone!</p>
<p>All I have to do is get this certificate on my Mac and I should be good. I copied the link to my Mac and…! Nothing. The link only works out of the SoftBank network and not through WiFi. Dang it.</p>
<h4 id="the-lastmile">The last mile</h4>
<p>What we got so far:</p>
<ol>
<li>SoftBank uses UA to show you the correct login page</li>
<li>Not authenticated users get a “please go to sbwifi.jp” page</li>
<li>sbwifi.jp only works out of the SoftBank LTE/3G network and sends a link to a profile through SMS to the users phone</li>
<li>The link to the profile, again, only works out of the SoftBank LTE/3G network</li>
</ol>
<p>So how do I get the profile on my Mac? The iPhone doesn’t let me download and share it through email and my Mac can’t download it. Just doesn’t work.</p>
<p>Luckily though, nowadays we can just enter the SoftBank network on our Mac! How? Through our phone of course!</p>
<p>To enable tethering on SoftBank iPhones you have to ‘purchase’ the tethering option through <a href="http://my.softbank.jp">my.softbank.jp</a>. Currently though that’s free for 2 years, so just enable it, wait a little bit and the “Personal Hotspot” option pops up in the settings.</p>
<p>
<img class="img-fluid" src="./1__WIP__dU6dVu8YxVWLah6LJw.png" alt="" />

</p>
<p>Creating a hotspot… connecting to it… and….</p>
<p>
<img class="img-fluid" src="./1__TztlUjmIz2fk3PfBGA7x4w.png" alt="" />

</p>
<p>Let’s try it again. UA switched to ‘iPhone’, navigating to sbwifi.jp, clicking the button and… yup there it is, the SMS to the profile. Copied to the Mac, clicked on it</p>
<p>
<img class="img-fluid" src="./1__2UwDS6KaQca5hnQjMJNfzg.png" alt="" />

</p>
<p>jackpot.</p>
<p>Luckily macOS is compatible with iPhone configuration profiles out of the box so we can go ahead and natively install it without any issues.</p>
<p>
<img class="img-fluid" src="./1__yr__YSchiNrRKvciUzd22jg.png" alt="" />

</p>
<p>Now all we have to do is disconnect from our iPhone, make sure the iPhone doesn’t automatically connect to SoftBank Wifi at the same time, connect to 0001softbank and tada! We have blazing fast internet :)</p>
<p>
<img class="img-fluid" src="./1__6__2qC__EnNt__EUrXjimDr4A.gif" alt="" />

</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Hello (Neo)Vim, good to see you again!</title>
      <link>https://david.coffee/hello-neovim-good-to-see-you-again/</link>
      <pubDate>Fri, 22 Jan 2016 10:30:23 +0000</pubDate>
      
      <guid>https://david.coffee/hello-neovim-good-to-see-you-again/</guid><description>&lt;p&gt;It’s weird that I am writing about editors so much but trying a new editor every few months kind of
became a tradition. It spices things up and allows me to learn something new that I maybe didn’t know yet before. Or just discover a new tool that improves my workflow.&lt;/p&gt;
&lt;p&gt;When I wrote about spacemacs, I thought spacemacs would be the thing that I would use for everything. Then I ported it to atom with &lt;a href=&#34;https://github.com/dvcrn/proton&#34;&gt;proton&lt;/a&gt; and thought that would be the tool I would be using for everything since I could just implement features on the spot that it didn’t have yet. So what happened that I am writing about editors again?&lt;/p&gt;
&lt;p&gt;As a VIM fan, I like micro optimizations. One thing in my workflow that I couldn’t stand was hammering &lt;code&gt;j&lt;/code&gt; &lt;code&gt;j&lt;/code&gt; &lt;code&gt;j&lt;/code&gt; &lt;code&gt;j&lt;/code&gt; to go down instead of &lt;code&gt;4j&lt;/code&gt;. I knew this keybinding was a lot faster, I just couldn’t get to teach myself to actually use it. After a bit of research I couldn’t find anything that helps me inside atom or emacs - but I found &lt;a href=&#34;https://github.com/wikitopian/hardmode&#34;&gt;hardmode for vim&lt;/a&gt;. It was a bit overkill since it disables all &lt;code&gt;hjkl&lt;/code&gt; keys but a good start. Then I found &lt;a href=&#34;https://github.com/takac/vim-hardtime&#34;&gt;hardtime&lt;/a&gt; and it was like my prayers have been answered 🙏. It allows me to use &lt;code&gt;hjkl&lt;/code&gt; exactly x times - and I can specify how much x is! Only problem - I need to use raw VIM.&lt;/p&gt;
&lt;h2 id=&#34;hello-neovim-again&#34;&gt;Hello NeoVim (again)&lt;/h2&gt;
&lt;p&gt;Instead of vim though I grabbed NeoVim as my weapon of choice. Stuffed in my old config file just… to realize that things are not working the way I want them to. Plugins were outdated, keybindings were ‘meh’ and my &lt;a href=&#34;https://github.com/dvcrn/dotfiles/blob/master/vim/.vimrc&#34;&gt;old vimrc&lt;/a&gt; a complete mess. How were I ever able to work with this??&lt;/p&gt;
&lt;p&gt;It had to go and something new had to be in it’s place, so I opened up a new vim pane and started hacking on my new vimrc with the goal that everything I use should be heavily optimized, consistent and use NeoVims async features if possible. No more &lt;a href=&#34;https://github.com/scrooloose/syntastic&#34;&gt;syntastic&lt;/a&gt; lags on linting - &lt;a href=&#34;https://github.com/benekastah/neomake&#34;&gt;neomake&lt;/a&gt; runs completely async!&lt;/p&gt;
&lt;p&gt;I replaced vundle with the async &lt;a href=&#34;https://github.com/junegunn/vim-plug&#34;&gt;vim-plug&lt;/a&gt; and re-added crucial packages that I needed - but something was still missing. I can’t just assign random leader keybindings after having such a blast with the spacemacs mnemonic. After changing the leader to &lt;code&gt;&amp;lt;SPC&amp;gt;&lt;/code&gt; and re-arranging the keybindings a bit I was quite surprised to see what I ended up doing - &lt;a href=&#34;https://github.com/dvcrn/dotfiles/blob/master/vim/nvim/keybindings.vim#L70-L112&#34;&gt;re-creating mnemonic keybindings in vim&lt;/a&gt;! I also went the extra steps and moved everything related into a package of some sorts. A bit of sourcing here and there and suddenly &lt;a href=&#34;https://github.com/dvcrn/dotfiles/blob/master/vim/nvim/.nvimrc#L79-L104&#34;&gt;I had “layers”&lt;/a&gt;! Just like in proton or spacemacs!&lt;/p&gt;
&lt;h2 id=&#34;quality-plugins-ahead&#34;&gt;Quality plugins ahead&lt;/h2&gt;
&lt;p&gt;What eventually got me really into vim were probably the amazing plugins of &lt;a href=&#34;https://github.com/junegunn/&#34;&gt;junegunn&lt;/a&gt;, the guy that wrote vim-plug. I am absolutely impressed about the quality of these and didn’t have this much fun with vim for a long time. Stuff like &lt;a href=&#34;https://github.com/junegunn/fzf&#34;&gt;fzf&lt;/a&gt; makes almost every command feel consistent.&lt;/p&gt;
&lt;p&gt;He even wrote a color scheme using the official colors of probably my favorite city on the planet - &lt;a href=&#34;https://github.com/junegunn/seoul256.vim&#34;&gt;Seoul&lt;/a&gt;. Just look how pretty it is:&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./neovim-1.png&#34; alt=&#34;screenshot&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;In all seriousness, his plugin collection is amazing and I can highly recommend you to check it out if you are using vim. He might be the next tpope!&lt;/p&gt;
&lt;h2 id=&#34;tldr&#34;&gt;tl;dr&lt;/h2&gt;
&lt;p&gt;hardtime for vim made me go back into vim territory and NeoVim made me stay.&lt;/p&gt;
&lt;p&gt;If you are a gvim guy like me, check out &lt;a href=&#34;https://github.com/equalsraf/neovim-qt&#34;&gt;neovim-qt&lt;/a&gt;. It is a lot faster performance wise and more stable than &lt;a href=&#34;https://github.com/rogual/neovim-dot-app&#34;&gt;Neovim.app&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Also my &lt;a href=&#34;https://github.com/dvcrn/dotfiles/tree/master/vim/nvim&#34;&gt;nvim dotfiles&lt;/a&gt; are a great starting point to get into vim. Everything is written very decoupled so you could just use whatever modules / packages you want and not source the rest.&lt;/p&gt;
&lt;p&gt;Do I now use vim for everyting? Nah, probably not. proton is still my main weapon. These days I&amp;rsquo;m on a VIM streak with the goal to master the more advanced text navigation commands but most of time time proton (if the atom-shell wouldn&amp;rsquo;t suck so much battery&amp;hellip;.).&lt;/p&gt;
&lt;p&gt;Let me know your thoughts &lt;a href=&#34;https://twitter.com/davicorn&#34;&gt;on twitter&lt;/a&gt;.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>It’s weird that I am writing about editors so much but trying a new editor every few months kind of
became a tradition. It spices things up and allows me to learn something new that I maybe didn’t know yet before. Or just discover a new tool that improves my workflow.</p>
<p>When I wrote about spacemacs, I thought spacemacs would be the thing that I would use for everything. Then I ported it to atom with <a href="https://github.com/dvcrn/proton">proton</a> and thought that would be the tool I would be using for everything since I could just implement features on the spot that it didn’t have yet. So what happened that I am writing about editors again?</p>
<p>As a VIM fan, I like micro optimizations. One thing in my workflow that I couldn’t stand was hammering <code>j</code> <code>j</code> <code>j</code> <code>j</code> to go down instead of <code>4j</code>. I knew this keybinding was a lot faster, I just couldn’t get to teach myself to actually use it. After a bit of research I couldn’t find anything that helps me inside atom or emacs - but I found <a href="https://github.com/wikitopian/hardmode">hardmode for vim</a>. It was a bit overkill since it disables all <code>hjkl</code> keys but a good start. Then I found <a href="https://github.com/takac/vim-hardtime">hardtime</a> and it was like my prayers have been answered 🙏. It allows me to use <code>hjkl</code> exactly x times - and I can specify how much x is! Only problem - I need to use raw VIM.</p>
<h2 id="hello-neovim-again">Hello NeoVim (again)</h2>
<p>Instead of vim though I grabbed NeoVim as my weapon of choice. Stuffed in my old config file just… to realize that things are not working the way I want them to. Plugins were outdated, keybindings were ‘meh’ and my <a href="https://github.com/dvcrn/dotfiles/blob/master/vim/.vimrc">old vimrc</a> a complete mess. How were I ever able to work with this??</p>
<p>It had to go and something new had to be in it’s place, so I opened up a new vim pane and started hacking on my new vimrc with the goal that everything I use should be heavily optimized, consistent and use NeoVims async features if possible. No more <a href="https://github.com/scrooloose/syntastic">syntastic</a> lags on linting - <a href="https://github.com/benekastah/neomake">neomake</a> runs completely async!</p>
<p>I replaced vundle with the async <a href="https://github.com/junegunn/vim-plug">vim-plug</a> and re-added crucial packages that I needed - but something was still missing. I can’t just assign random leader keybindings after having such a blast with the spacemacs mnemonic. After changing the leader to <code>&lt;SPC&gt;</code> and re-arranging the keybindings a bit I was quite surprised to see what I ended up doing - <a href="https://github.com/dvcrn/dotfiles/blob/master/vim/nvim/keybindings.vim#L70-L112">re-creating mnemonic keybindings in vim</a>! I also went the extra steps and moved everything related into a package of some sorts. A bit of sourcing here and there and suddenly <a href="https://github.com/dvcrn/dotfiles/blob/master/vim/nvim/.nvimrc#L79-L104">I had “layers”</a>! Just like in proton or spacemacs!</p>
<h2 id="quality-plugins-ahead">Quality plugins ahead</h2>
<p>What eventually got me really into vim were probably the amazing plugins of <a href="https://github.com/junegunn/">junegunn</a>, the guy that wrote vim-plug. I am absolutely impressed about the quality of these and didn’t have this much fun with vim for a long time. Stuff like <a href="https://github.com/junegunn/fzf">fzf</a> makes almost every command feel consistent.</p>
<p>He even wrote a color scheme using the official colors of probably my favorite city on the planet - <a href="https://github.com/junegunn/seoul256.vim">Seoul</a>. Just look how pretty it is:</p>
<p>
<img class="img-fluid" src="./neovim-1.png" alt="screenshot" />

</p>
<p>In all seriousness, his plugin collection is amazing and I can highly recommend you to check it out if you are using vim. He might be the next tpope!</p>
<h2 id="tldr">tl;dr</h2>
<p>hardtime for vim made me go back into vim territory and NeoVim made me stay.</p>
<p>If you are a gvim guy like me, check out <a href="https://github.com/equalsraf/neovim-qt">neovim-qt</a>. It is a lot faster performance wise and more stable than <a href="https://github.com/rogual/neovim-dot-app">Neovim.app</a>.</p>
<p>Also my <a href="https://github.com/dvcrn/dotfiles/tree/master/vim/nvim">nvim dotfiles</a> are a great starting point to get into vim. Everything is written very decoupled so you could just use whatever modules / packages you want and not source the rest.</p>
<p>Do I now use vim for everyting? Nah, probably not. proton is still my main weapon. These days I&rsquo;m on a VIM streak with the goal to master the more advanced text navigation commands but most of time time proton (if the atom-shell wouldn&rsquo;t suck so much battery&hellip;.).</p>
<p>Let me know your thoughts <a href="https://twitter.com/davicorn">on twitter</a>.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Sweet, sweet Elixir - the language of my dreams?</title>
      <link>https://david.coffee/sweet-sweet-elixir-the-language-of-my-dreams/</link>
      <pubDate>Fri, 22 Jan 2016 10:30:23 +0000</pubDate>
      
      <guid>https://david.coffee/sweet-sweet-elixir-the-language-of-my-dreams/</guid><description>&lt;p&gt;I like learning new languages, I think that’s no secret. It’s a fun task that teaches you new interesting takes on existing problems. A language can make your programmers life very easy but could also turn it into a living hell. In fact, I am always on the hunt to find the &lt;em&gt;perfect&lt;/em&gt; language for me. That thing that I &lt;strong&gt;want&lt;/strong&gt; to use for everything.&lt;/p&gt;
&lt;h3 id=&#34;meet-clojurescript&#34;&gt;Meet ClojureScript&lt;/h3&gt;
&lt;p&gt;Lately I’ve been doing a lot of clojure. I am glad that I finally arrived at a point where I don’t have to look into the documentation just to find out how to append something to a vector. Clojure is &lt;strong&gt;extremely well&lt;/strong&gt; designed. I learned a lot just by listening to talks (especially the rich hickey ones!) and reading about the languages&amp;rsquo; core concepts and the reasons behind them. But above all, Clojure is just fun to use and ClojureScript is even more fun! I love using it and even wrote a &lt;a href=&#34;https://github.com/dvcrn/markright&#34;&gt;MarkDown editor&lt;/a&gt; in it. Lately clojurescript ended up becoming the magic that fueles my &lt;a href=&#34;https://github.com/dvcrn/proton&#34;&gt;entire main editor&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;What’s amazing about ClojureScript is that it can literally go everywhere javascript can go and man can javascript go to places these days. I even started to build freaking &lt;a href=&#34;https://dvcrn.github.io/clojurescript/react/2015/11/19/my-cljs-talk.html&#34;&gt;native apps&lt;/a&gt; with it! Imagine I would tell that to someone a few years back:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;A: Oh that app looks cool. How did you build it?
B: Ah I’m using clojurescript to control the native view properties.
A: cloj… what?
B: Yeah, it cross compiles to javascript.
A: Javascript? native?
B: Javascript is then using react native to bridge into ObjC
A: 😮
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I like clojurescript to that degree that I start to feel bothered if someone asks me to write raw javascript these days. I like it that much.&lt;/p&gt;
&lt;h3 id=&#34;clojure&#34;&gt;Clojure&lt;/h3&gt;
&lt;p&gt;The only thing I am not 100% happy with right now is… well… clojure! I tried a few times to use it on the serverside but it just never felt completely right to me. I ended up searching all these little building blocks like migrations, database connections, router and templating, and proceeded to stack them together… somehow.&lt;/p&gt;
&lt;p&gt;Then there was the big problem of libraries getting abandoned. Here’s a example: When I wrote my first server it took me ages to find &lt;a href=&#34;https://github.com/budu/lobos&#34;&gt;lobos&lt;/a&gt; - the migration library everyone told me to use. Shortly after, it turned inactive and communication around it died. Suddenly noone wanted to use lobos. A few questions here and there and people told me to &lt;em&gt;just&lt;/em&gt; write SQL files and pipe them with clojure into the db.&lt;/p&gt;
&lt;p&gt;Clojure people feel to me like these people that like to build stuff raw and by themselves. To have full control over every aspect and if a building block doesn&amp;rsquo;t exist yet - it will just get written on the spot.
My server eventually worked and that is what counts, right? But I wasn’t happy maintaining it. All these lego bricks felt glued together in a not stable way.&lt;/p&gt;
&lt;p&gt;So I kept going on my way, sometimes using clojure, but most of the times django for my server stuff. I kept adding things to my &lt;em&gt;you-should-check-that-out-list&lt;/em&gt; including Go, Rust, and a few frameworks here and there, just waiting for me to have some more free time.&lt;/p&gt;
&lt;p&gt;A few days ago I wanted to start a new side project - an app to be specific. My fingers were itching to do a real world app in clojurescript but needed something for the server. I started mocking things up with django but then took a short look at my list. On first place was something called &lt;em&gt;elixir&lt;/em&gt;. “Elixir, huh?”, I thought and opened the website&lt;/p&gt;
&lt;h3 id=&#34;hello-elixir&#34;&gt;Hello Elixir&lt;/h3&gt;
&lt;p&gt;When I first checked out Elixir I didn’t think of much. Looks like ruby and feels like ruby with a bit of clojure sugar sprinkled in. The &lt;code&gt;|&amp;gt;&lt;/code&gt; operator is obviously taken straight out it. Atoms are no longer state containers but now what I would call… a keyword? In any way, being a functional language inspired by clojure and coming directly &lt;em&gt;from&lt;/em&gt; clojure I felt home right away.&lt;/p&gt;
&lt;p&gt;I downloaded &lt;a href=&#34;http://www.phoenixframework.org&#34;&gt;phoenix&lt;/a&gt;, figured out &lt;a href=&#34;https://github.com/elixir-lang/ecto&#34;&gt;ecto&lt;/a&gt;, hacked my API server together and went on with my app. That was until I had to implement a image upload and resize service to s3.&lt;/p&gt;
&lt;p&gt;My django instinct told me to find the &lt;a href=&#34;http://celeryproject.org&#34;&gt;celery&lt;/a&gt; of the elixir world but what came next completely changed my way I looked at the language.&lt;/p&gt;
&lt;p&gt;You see - I didn’t read the entire documentation on elixir-lang (which I should have done). I wanted to get a feel for the language first before going into all the little details. When I asked on the elixir slack for the idiomatic way of implementing a worker I got pointed to the &lt;a href=&#34;http://elixir-lang.org/getting-started/processes.html&#34;&gt;processes&lt;/a&gt;, &lt;a href=&#34;http://elixir-lang.org/getting-started/mix-otp/agent.html&#34;&gt;agents&lt;/a&gt; and &lt;a href=&#34;http://elixir-lang.org/getting-started/mix-otp/genserver.html&#34;&gt;genserver&lt;/a&gt; guides.&lt;/p&gt;
&lt;p&gt;I knew that elixir is running inside the erlang VM but I never used erlang before and all I knew about it was from hearing about it.&lt;/p&gt;
&lt;h3 id=&#34;mind--&#34;&gt;Mind = 💥&lt;/h3&gt;
&lt;p&gt;To summarise it very short: The concurrency handling of elixir is just effing amazing. Without any troubles and at any time you can spin up subprocesses, pass stuff to them, link them together and build your little network.&lt;/p&gt;
&lt;p&gt;Processes are completely isolated and very well managed. I never had the feeling that using processes will now lock me into a big headache network of synchronisation, orchestration and possible chaos. And even if something goes wrong - elixir and erlang actually &lt;em&gt;encourage&lt;/em&gt; you to let things fail! No try/catch blocks and exception handling. You just let the process fail and the supervisor will take care of troubleshooting and restart that thing - just like that!&lt;/p&gt;
&lt;p&gt;Here’s an example that really shows how powerful this is: State management! The recommended way of storing state in elixir is… a process! You have a process that holds your piece of state and reacts to defined messages passed to it: e.g. &lt;em&gt;store&lt;/em&gt; and &lt;em&gt;retrieve&lt;/em&gt;. I mean… what?!&lt;/p&gt;
&lt;p&gt;If you now connect another node to your mesh network, that node could immediately send messages to the same state management process if it knows it’s identifier (e.g. &lt;code&gt;:kwstore1&lt;/code&gt;). Just like that you suddenly have state management - but synchronised on multiple machines… I mean… what?!?!&lt;/p&gt;
&lt;h3 id=&#34;the-joy-of-concurrency-and-even-more---&#34;&gt;The joy of concurrency and even more 😮 = 💥&lt;/h3&gt;
&lt;p&gt;Elixir doesn’t care if you have 1 node or a thousand. It directly works with all of them right away.&lt;/p&gt;
&lt;p&gt;Let’s go back to my image upload and resize example: In django I would take the image, push it to s3, push a message into amqp / redis, tell the user “success” and let a worker process the resizing before marking the image as “processed” in a db. 3 components: Web server, queue server, worker.&lt;/p&gt;
&lt;p&gt;Now elixir. I take the image, put it on the disk and push the original to s3. I tell the user “success” but at the &lt;em&gt;same time&lt;/em&gt; spin up a child process with the reference to image on the disk. The user gets his “success” but the child process lives on! It does it’s resizing stuff, uploads to s3, updates the db and if something goes wrong like a timeout from s3, then so what? It just crashes, the supervisor restarts the process and it tries again. All while the user is already gone! Without a queue! Without a worker! Out of the box!&lt;/p&gt;
&lt;p&gt;To give that more context, on heroku with django I would have to scale up another worker process (costs $), add a amqp server (even more $) and let everything run at the same time. On heroku with elixir I can literally have all of that inside a free dyno! I could even spin up 50 worker processes and it could still run inside the same free dyno without a cent extra if I am nice with memory.&lt;/p&gt;
&lt;p&gt;And you know actually how easy it is to spawn a simple child process?&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;spawn fn -&amp;gt;
	IO.puts “Hello from Child”
end
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;But I’m just scratching the surface of this amazing language. Phoenix for example has built in support for websockets, you know what this means when it comes to elixir? You could connect to server1, I could connect to server98934 which is part of the same mesh and we could &lt;strong&gt;still&lt;/strong&gt; chat with each other without any extra work!&lt;/p&gt;
&lt;p&gt;Elixir made it damn appealing to use concurrency programming. I never fear that I will break something essential because it is just this well done. It is designed that it actually encourages you to use these features more often - because seriously, why wouldn’t you?&lt;/p&gt;
&lt;h3 id=&#34;scripted-elixir&#34;&gt;Scripted Elixir&lt;/h3&gt;
&lt;p&gt;Amazing syntax, functional programming, REPL driven development, unbeatable concurrency model - how could you make this language even more attractive? I will tell you how:&lt;/p&gt;
&lt;p&gt;Elixir has 2 ways of executing your code: Pre-compiled and scripted. Pre-compiled means that once you are ready to run your app you tell elixir (or mix) to compile everything down and execute it. But what could &lt;em&gt;scripted&lt;/em&gt; mean?&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;[i] λ elixir -e ‘IO.puts “Hello World”’
Hello World
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;…that’s what it means! Elixir comes with a &lt;a href=&#34;http://elixir-lang.org/getting-started/modules.html#scripted-mode&#34;&gt;scripted mode&lt;/a&gt; that compiles your code on the spot in memory before executing it! No slow spinning up java to execute your clojure code or compile it into javascript. Executing elixir in scripted mode takes 1 second at max and doesn’t require you to have anything set up. No project, no mix - nothing.&lt;/p&gt;
&lt;h3 id=&#34;the-language-of-my-dreams&#34;&gt;The language of my dreams?&lt;/h3&gt;
&lt;p&gt;I know it is too early to judge but I really like Elixir. I didn’t have this much fun programming on the server since I initially discovered django. Pair Elixir on the server with clojurescript on the client and you have the stack of my dreams - powerful and fun programming on the client and server at the same time (let’s call it &lt;em&gt;dave-stack&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;I will continue my journey with Elixir. If you didn’t check it out - I highly urge you to do so. It could very easily become the next ruby in a very short time.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>I like learning new languages, I think that’s no secret. It’s a fun task that teaches you new interesting takes on existing problems. A language can make your programmers life very easy but could also turn it into a living hell. In fact, I am always on the hunt to find the <em>perfect</em> language for me. That thing that I <strong>want</strong> to use for everything.</p>
<h3 id="meet-clojurescript">Meet ClojureScript</h3>
<p>Lately I’ve been doing a lot of clojure. I am glad that I finally arrived at a point where I don’t have to look into the documentation just to find out how to append something to a vector. Clojure is <strong>extremely well</strong> designed. I learned a lot just by listening to talks (especially the rich hickey ones!) and reading about the languages&rsquo; core concepts and the reasons behind them. But above all, Clojure is just fun to use and ClojureScript is even more fun! I love using it and even wrote a <a href="https://github.com/dvcrn/markright">MarkDown editor</a> in it. Lately clojurescript ended up becoming the magic that fueles my <a href="https://github.com/dvcrn/proton">entire main editor</a>.</p>
<p>What’s amazing about ClojureScript is that it can literally go everywhere javascript can go and man can javascript go to places these days. I even started to build freaking <a href="https://dvcrn.github.io/clojurescript/react/2015/11/19/my-cljs-talk.html">native apps</a> with it! Imagine I would tell that to someone a few years back:</p>
<pre tabindex="0"><code>A: Oh that app looks cool. How did you build it?
B: Ah I’m using clojurescript to control the native view properties.
A: cloj… what?
B: Yeah, it cross compiles to javascript.
A: Javascript? native?
B: Javascript is then using react native to bridge into ObjC
A: 😮
</code></pre><p>I like clojurescript to that degree that I start to feel bothered if someone asks me to write raw javascript these days. I like it that much.</p>
<h3 id="clojure">Clojure</h3>
<p>The only thing I am not 100% happy with right now is… well… clojure! I tried a few times to use it on the serverside but it just never felt completely right to me. I ended up searching all these little building blocks like migrations, database connections, router and templating, and proceeded to stack them together… somehow.</p>
<p>Then there was the big problem of libraries getting abandoned. Here’s a example: When I wrote my first server it took me ages to find <a href="https://github.com/budu/lobos">lobos</a> - the migration library everyone told me to use. Shortly after, it turned inactive and communication around it died. Suddenly noone wanted to use lobos. A few questions here and there and people told me to <em>just</em> write SQL files and pipe them with clojure into the db.</p>
<p>Clojure people feel to me like these people that like to build stuff raw and by themselves. To have full control over every aspect and if a building block doesn&rsquo;t exist yet - it will just get written on the spot.
My server eventually worked and that is what counts, right? But I wasn’t happy maintaining it. All these lego bricks felt glued together in a not stable way.</p>
<p>So I kept going on my way, sometimes using clojure, but most of the times django for my server stuff. I kept adding things to my <em>you-should-check-that-out-list</em> including Go, Rust, and a few frameworks here and there, just waiting for me to have some more free time.</p>
<p>A few days ago I wanted to start a new side project - an app to be specific. My fingers were itching to do a real world app in clojurescript but needed something for the server. I started mocking things up with django but then took a short look at my list. On first place was something called <em>elixir</em>. “Elixir, huh?”, I thought and opened the website</p>
<h3 id="hello-elixir">Hello Elixir</h3>
<p>When I first checked out Elixir I didn’t think of much. Looks like ruby and feels like ruby with a bit of clojure sugar sprinkled in. The <code>|&gt;</code> operator is obviously taken straight out it. Atoms are no longer state containers but now what I would call… a keyword? In any way, being a functional language inspired by clojure and coming directly <em>from</em> clojure I felt home right away.</p>
<p>I downloaded <a href="http://www.phoenixframework.org">phoenix</a>, figured out <a href="https://github.com/elixir-lang/ecto">ecto</a>, hacked my API server together and went on with my app. That was until I had to implement a image upload and resize service to s3.</p>
<p>My django instinct told me to find the <a href="http://celeryproject.org">celery</a> of the elixir world but what came next completely changed my way I looked at the language.</p>
<p>You see - I didn’t read the entire documentation on elixir-lang (which I should have done). I wanted to get a feel for the language first before going into all the little details. When I asked on the elixir slack for the idiomatic way of implementing a worker I got pointed to the <a href="http://elixir-lang.org/getting-started/processes.html">processes</a>, <a href="http://elixir-lang.org/getting-started/mix-otp/agent.html">agents</a> and <a href="http://elixir-lang.org/getting-started/mix-otp/genserver.html">genserver</a> guides.</p>
<p>I knew that elixir is running inside the erlang VM but I never used erlang before and all I knew about it was from hearing about it.</p>
<h3 id="mind--">Mind = 💥</h3>
<p>To summarise it very short: The concurrency handling of elixir is just effing amazing. Without any troubles and at any time you can spin up subprocesses, pass stuff to them, link them together and build your little network.</p>
<p>Processes are completely isolated and very well managed. I never had the feeling that using processes will now lock me into a big headache network of synchronisation, orchestration and possible chaos. And even if something goes wrong - elixir and erlang actually <em>encourage</em> you to let things fail! No try/catch blocks and exception handling. You just let the process fail and the supervisor will take care of troubleshooting and restart that thing - just like that!</p>
<p>Here’s an example that really shows how powerful this is: State management! The recommended way of storing state in elixir is… a process! You have a process that holds your piece of state and reacts to defined messages passed to it: e.g. <em>store</em> and <em>retrieve</em>. I mean… what?!</p>
<p>If you now connect another node to your mesh network, that node could immediately send messages to the same state management process if it knows it’s identifier (e.g. <code>:kwstore1</code>). Just like that you suddenly have state management - but synchronised on multiple machines… I mean… what?!?!</p>
<h3 id="the-joy-of-concurrency-and-even-more---">The joy of concurrency and even more 😮 = 💥</h3>
<p>Elixir doesn’t care if you have 1 node or a thousand. It directly works with all of them right away.</p>
<p>Let’s go back to my image upload and resize example: In django I would take the image, push it to s3, push a message into amqp / redis, tell the user “success” and let a worker process the resizing before marking the image as “processed” in a db. 3 components: Web server, queue server, worker.</p>
<p>Now elixir. I take the image, put it on the disk and push the original to s3. I tell the user “success” but at the <em>same time</em> spin up a child process with the reference to image on the disk. The user gets his “success” but the child process lives on! It does it’s resizing stuff, uploads to s3, updates the db and if something goes wrong like a timeout from s3, then so what? It just crashes, the supervisor restarts the process and it tries again. All while the user is already gone! Without a queue! Without a worker! Out of the box!</p>
<p>To give that more context, on heroku with django I would have to scale up another worker process (costs $), add a amqp server (even more $) and let everything run at the same time. On heroku with elixir I can literally have all of that inside a free dyno! I could even spin up 50 worker processes and it could still run inside the same free dyno without a cent extra if I am nice with memory.</p>
<p>And you know actually how easy it is to spawn a simple child process?</p>
<pre tabindex="0"><code>spawn fn -&gt;
	IO.puts “Hello from Child”
end
</code></pre><p>But I’m just scratching the surface of this amazing language. Phoenix for example has built in support for websockets, you know what this means when it comes to elixir? You could connect to server1, I could connect to server98934 which is part of the same mesh and we could <strong>still</strong> chat with each other without any extra work!</p>
<p>Elixir made it damn appealing to use concurrency programming. I never fear that I will break something essential because it is just this well done. It is designed that it actually encourages you to use these features more often - because seriously, why wouldn’t you?</p>
<h3 id="scripted-elixir">Scripted Elixir</h3>
<p>Amazing syntax, functional programming, REPL driven development, unbeatable concurrency model - how could you make this language even more attractive? I will tell you how:</p>
<p>Elixir has 2 ways of executing your code: Pre-compiled and scripted. Pre-compiled means that once you are ready to run your app you tell elixir (or mix) to compile everything down and execute it. But what could <em>scripted</em> mean?</p>
<pre tabindex="0"><code>[i] λ elixir -e ‘IO.puts “Hello World”’
Hello World
</code></pre><p>…that’s what it means! Elixir comes with a <a href="http://elixir-lang.org/getting-started/modules.html#scripted-mode">scripted mode</a> that compiles your code on the spot in memory before executing it! No slow spinning up java to execute your clojure code or compile it into javascript. Executing elixir in scripted mode takes 1 second at max and doesn’t require you to have anything set up. No project, no mix - nothing.</p>
<h3 id="the-language-of-my-dreams">The language of my dreams?</h3>
<p>I know it is too early to judge but I really like Elixir. I didn’t have this much fun programming on the server since I initially discovered django. Pair Elixir on the server with clojurescript on the client and you have the stack of my dreams - powerful and fun programming on the client and server at the same time (let’s call it <em>dave-stack</em>).</p>
<p>I will continue my journey with Elixir. If you didn’t check it out - I highly urge you to do so. It could very easily become the next ruby in a very short time.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Spacemacs for atom - a few weeks later</title>
      <link>https://david.coffee/spacemacs-for-atom-a-few-weeks-later/</link>
      <pubDate>Thu, 10 Dec 2015 11:59:23 +0000</pubDate>
      
      <guid>https://david.coffee/spacemacs-for-atom-a-few-weeks-later/</guid><description>&lt;p&gt;I recently &lt;a href=&#34;https://david.coffee/clojurescript/2015/11/27/spacemacs-for-atom.html&#34;&gt;wrote&lt;/a&gt; about my attempt to bring spacemacs style editing to atom. A lot of stuff happened that I wanna tell you about!&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./UmxjocD.gif&#34; alt=&#34;showcase&#34; /&gt;

&lt;/p&gt;
&lt;h3 id=&#34;in-review&#34;&gt;In review&lt;/h3&gt;
&lt;p&gt;Since the last post, &lt;a href=&#34;https://github.com/dvcrn/proton&#34;&gt;proton&lt;/a&gt; acquired 72 stars, 13 watches and 4 forks. We now have 3 people that actively contribute changes.&lt;/p&gt;
&lt;p&gt;Proton became a good amount more stable, removed a lot of the ugly hacks it had in the beginning and added some really cool stuff on top. Let me give you a short overview:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We started creating a better default experience. The goal is that you just install proton, it reads your &lt;code&gt;~/.proton&lt;/code&gt; and bam! You have a superb editor experience. Since 0.1.2 we ship a better theme, a better font, better default settings and put a lot more power into the dotfile.&lt;/li&gt;
&lt;li&gt;Step by step we are trying to allow users to toggle the entire atom experience from within &lt;code&gt;~/.proton&lt;/code&gt; or through the &lt;code&gt;&amp;lt;spc&amp;gt; t&lt;/code&gt; menu. We created the base for pulling stuff like &lt;code&gt;&amp;lt;spc&amp;gt; t n&lt;/code&gt; for relative line numbers off (which by the way is in a third party package and not part of atoms core) or &lt;code&gt;&amp;lt;spc&amp;gt; t t&lt;/code&gt; to disable the tab-bar (which &lt;strong&gt;is&lt;/strong&gt; a core package).&lt;/li&gt;
&lt;li&gt;You can swap-out components more easily. Say you like &lt;a href=&#34;http://nuclide.io/&#34;&gt;nuclide&lt;/a&gt; and you want to use their quick-open tool instead of the default one. Now all you have to do is swap &lt;code&gt;proton.core.quickOpenProvider&lt;/code&gt; to &lt;code&gt;:nuclide&lt;/code&gt; and proton will replace it. Following this, adding different providers like swapping the file-tree with something else in the future will be no problem.&lt;/li&gt;
&lt;li&gt;We have context aware shortcuts and modes &lt;a href=&#34;https://github.com/dvcrn/proton/pull/27&#34;&gt;on the way&lt;/a&gt;! This means that your &lt;code&gt;&amp;lt;spc&amp;gt;&lt;/code&gt; menu could change depending on what file you are editing. &lt;code&gt;&amp;lt;spc&amp;gt; m =&lt;/code&gt; in python could execute yapf while in javascript, it could use js-beautify. This would make proton a truly dynamic experience.&lt;/li&gt;
&lt;li&gt;We added a handful new &lt;a href=&#34;https://github.com/dvcrn/proton/tree/master/src/cljs/proton/layers&#34;&gt;layers&lt;/a&gt; with more being added over time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The crazy thing is that proton already bypassed &lt;a href=&#34;https://github.com/dvcrn/sublimious&#34;&gt;sublimious&lt;/a&gt; in popularity, functionality and features.&lt;/p&gt;
&lt;h3 id=&#34;live-reloading-inside-atom&#34;&gt;Live reloading inside Atom&lt;/h3&gt;
&lt;p&gt;Thanks to the amazing &lt;a href=&#34;https://github.com/thheller&#34;&gt;thheller&lt;/a&gt; we now even have full support for hot code reloading while developing proton. It goes even this far that we might even be one of the onlys plugin out there that was able to pull this off with full REPL integration! Even the guys at the &lt;a href=&#34;http://discuss.atom.io&#34;&gt;atom forums&lt;/a&gt; couldn&amp;rsquo;t do that 😁&lt;/p&gt;
&lt;p&gt;With this new and even more stable build process, creating plugins in atom has never been this fun and easy! (&lt;a href=&#34;https://github.com/dvcrn/ajom&#34;&gt;ajom&lt;/a&gt; has been updated with the latest changes.)&lt;/p&gt;
&lt;h3 id=&#34;the-future&#34;&gt;The future&lt;/h3&gt;
&lt;p&gt;I am very impressed to see how proton is doing. As assumed, clojurescript is making atom such a powerful tool with a lot of unused potential. With &lt;a href=&#34;https://github.com/dvcrn/proton/pull/27&#34;&gt;modes-ish&lt;/a&gt; stuff on the way, we could even see something similar to emacs&amp;rsquo;s modes in the future. Just imagine that, composing modes inside atom! (Just wondering when the atom API will become limiting&amp;hellip;)&lt;/p&gt;
&lt;p&gt;Proton is on a steady way to become your central gateway when interacting with atom. Leveraging clojure goodies and now even support for live reloading and REPL development - developing a atom extension has never been this fun.&lt;/p&gt;
&lt;p&gt;Now just to hope that more people adopt this and we might see more high quality and faster developed extensions on the way.&lt;/p&gt;
&lt;p&gt;Spacemacs, we are coming for you!&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>I recently <a href="https://david.coffee/clojurescript/2015/11/27/spacemacs-for-atom.html">wrote</a> about my attempt to bring spacemacs style editing to atom. A lot of stuff happened that I wanna tell you about!</p>
<p>
<img class="img-fluid" src="./UmxjocD.gif" alt="showcase" />

</p>
<h3 id="in-review">In review</h3>
<p>Since the last post, <a href="https://github.com/dvcrn/proton">proton</a> acquired 72 stars, 13 watches and 4 forks. We now have 3 people that actively contribute changes.</p>
<p>Proton became a good amount more stable, removed a lot of the ugly hacks it had in the beginning and added some really cool stuff on top. Let me give you a short overview:</p>
<ul>
<li>We started creating a better default experience. The goal is that you just install proton, it reads your <code>~/.proton</code> and bam! You have a superb editor experience. Since 0.1.2 we ship a better theme, a better font, better default settings and put a lot more power into the dotfile.</li>
<li>Step by step we are trying to allow users to toggle the entire atom experience from within <code>~/.proton</code> or through the <code>&lt;spc&gt; t</code> menu. We created the base for pulling stuff like <code>&lt;spc&gt; t n</code> for relative line numbers off (which by the way is in a third party package and not part of atoms core) or <code>&lt;spc&gt; t t</code> to disable the tab-bar (which <strong>is</strong> a core package).</li>
<li>You can swap-out components more easily. Say you like <a href="http://nuclide.io/">nuclide</a> and you want to use their quick-open tool instead of the default one. Now all you have to do is swap <code>proton.core.quickOpenProvider</code> to <code>:nuclide</code> and proton will replace it. Following this, adding different providers like swapping the file-tree with something else in the future will be no problem.</li>
<li>We have context aware shortcuts and modes <a href="https://github.com/dvcrn/proton/pull/27">on the way</a>! This means that your <code>&lt;spc&gt;</code> menu could change depending on what file you are editing. <code>&lt;spc&gt; m =</code> in python could execute yapf while in javascript, it could use js-beautify. This would make proton a truly dynamic experience.</li>
<li>We added a handful new <a href="https://github.com/dvcrn/proton/tree/master/src/cljs/proton/layers">layers</a> with more being added over time.</li>
</ul>
<p>The crazy thing is that proton already bypassed <a href="https://github.com/dvcrn/sublimious">sublimious</a> in popularity, functionality and features.</p>
<h3 id="live-reloading-inside-atom">Live reloading inside Atom</h3>
<p>Thanks to the amazing <a href="https://github.com/thheller">thheller</a> we now even have full support for hot code reloading while developing proton. It goes even this far that we might even be one of the onlys plugin out there that was able to pull this off with full REPL integration! Even the guys at the <a href="http://discuss.atom.io">atom forums</a> couldn&rsquo;t do that 😁</p>
<p>With this new and even more stable build process, creating plugins in atom has never been this fun and easy! (<a href="https://github.com/dvcrn/ajom">ajom</a> has been updated with the latest changes.)</p>
<h3 id="the-future">The future</h3>
<p>I am very impressed to see how proton is doing. As assumed, clojurescript is making atom such a powerful tool with a lot of unused potential. With <a href="https://github.com/dvcrn/proton/pull/27">modes-ish</a> stuff on the way, we could even see something similar to emacs&rsquo;s modes in the future. Just imagine that, composing modes inside atom! (Just wondering when the atom API will become limiting&hellip;)</p>
<p>Proton is on a steady way to become your central gateway when interacting with atom. Leveraging clojure goodies and now even support for live reloading and REPL development - developing a atom extension has never been this fun.</p>
<p>Now just to hope that more people adopt this and we might see more high quality and faster developed extensions on the way.</p>
<p>Spacemacs, we are coming for you!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Spacemacs for Atom</title>
      <link>https://david.coffee/spacemacs-for-atom/</link>
      <pubDate>Fri, 27 Nov 2015 18:24:23 +0000</pubDate>
      
      <guid>https://david.coffee/spacemacs-for-atom/</guid><description>&lt;p&gt;If you read my last posts you might notice that I really like editors. Something about finding better and better tools is very interesting and fun. Plus using a new tool for a couple of weeks is keeping things fresh even when you work on the same codebase.&lt;/p&gt;
&lt;p&gt;The problem is that since I started using &lt;a href=&#34;http://dvcrn.github.io/2015/10/13/my_dream_editor.html&#34;&gt;spacemacs&lt;/a&gt;, I can&amp;rsquo;t really use anything else. It just&amp;hellip; doesn&amp;rsquo;t work. The shortcuts are too awkward, get overriden by third party plugins and all the manual config orchestration is just a pain.&lt;/p&gt;
&lt;p&gt;When I went back to Sublime Text for a little, I ended up writing &lt;a href=&#34;https://github.com/dvcrn/sublimious&#34;&gt;sublimious&lt;/a&gt;, a plugin which ports a big chunk of the spacemacs toolset into Sublime Text:&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./MqsB6Pt.gif&#34; alt=&#34;sublimious&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;It works &lt;strong&gt;really REALLY good&lt;/strong&gt;! But eventually I ran into problems implementing more things. Stuff like relative line numbers is close to impossible to do without a big performance hit or awkward behavior. Or the gutter still only being able to hold 1 element in it. What&amp;rsquo;s up with that?&lt;/p&gt;
&lt;p&gt;Performance wise it is crazy fast but let&amp;rsquo;s face it: Sublime Text is dead even if the community doesn&amp;rsquo;t want to admit it. Waiting until John Skinner to come along and commit a new feature once a year is just taking too long.&lt;/p&gt;
&lt;h3 id=&#34;my-first-time-with-atom&#34;&gt;My first time with Atom?&lt;/h3&gt;
&lt;p&gt;Now I am a little bit biased towards atom. I looked at it multiple times but it felt too slow, too in-performant and most of the plugins were just trash-level (sorry if I insulted some of my readers but they are just&amp;hellip; not good!).&lt;/p&gt;
&lt;p&gt;All the problems I have about plugins overwriting keybindings are here stronger than anywhere else. A plugin can literally change every little aspect of the editor. This is a really great thing, don&amp;rsquo;t get me wrong! But at the same time you need to take double care of what you are installing.&lt;/p&gt;
&lt;p&gt;Github also did a lot of weird design decicions that I can&amp;rsquo;t follow. Here are a few:&lt;/p&gt;
&lt;h5 id=&#34;emoji-as-commit-messages&#34;&gt;Emoji as commit messages&lt;/h5&gt;
&lt;p&gt;I tried to nibble my way through the codebase and recent changes a bit but emojis&amp;hellip; man. I mean, it&amp;rsquo;s cool that you have fun with it but for outsiders it&amp;rsquo;s unnecessary confusing. 🎨 is probably style related, I get that, but what the heck is 📝? 🔥?&lt;/p&gt;
&lt;p&gt;Plus using emojis is not a substitute for a commit message. It&amp;rsquo;s like me commiting with the word &amp;ldquo;style.&amp;rdquo;&lt;/p&gt;
&lt;h5 id=&#34;coffeescript&#34;&gt;Coffeescript&lt;/h5&gt;
&lt;p&gt;I used to like coffeescript a lot but reading through the atom sourcecode made me realise why I will probably not touch it again.&lt;/p&gt;
&lt;p&gt;Take this snippet for example:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-coffee&#34; data-lang=&#34;coffee&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@eventElement&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;dispatchEvent&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;CustomEvent&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;name&lt;/span&gt;, bubbles: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;, cancelable: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What is it doing? Why do some of the arguments have colons? Where does the object start and where does it end? The constructor is getting 3 arguments, right? Right?&lt;/p&gt;
&lt;p&gt;I literally had to run this through a coffeescript parser to understand what it&amp;rsquo;s doing:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;eventElement&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;dispatchEvent&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;CustomEvent&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;name&lt;/span&gt;, {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;bubbles&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;cancelable&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h5 id=&#34;confusing-objects-everywhere&#34;&gt;confusing objects everywhere&lt;/h5&gt;
&lt;p&gt;I&amp;rsquo;m a big functional guy and everywhere I go in the atom sourcecode, I am dealing with objects after objects. I can&amp;rsquo;t count how many &lt;code&gt;@element&lt;/code&gt; assignments and reads I saw. Some things are getting assigned at the top of an object, used at the bottom and changed 3x until it reached that spot.&lt;/p&gt;
&lt;h5 id=&#34;cson&#34;&gt;cson&lt;/h5&gt;
&lt;p&gt;For some reason someone decided that coffeescript is so great that we should also start writing json in it! Because&amp;hellip; commas are evil or something?&lt;/p&gt;
&lt;p&gt;I quickly ran away from it&lt;/p&gt;
&lt;h3 id=&#34;atom-meet-clojurescript&#34;&gt;Atom, meet clojurescript&lt;/h3&gt;
&lt;p&gt;Now, since then and now I got a lot better in clojurescript so I decided to give it another go. I created a little &lt;a href=&#34;https://github.com/dvcrn/ajom&#34;&gt;lein template&lt;/a&gt; for creating atom plugins in clojurescript and hacked a bit here and there.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Turns out this works really well!&lt;/strong&gt; Clojurescript and atom are like&amp;hellip; emacs and elisp! You have all the power of clojurescript combined with all the customisation options atom gives you. You still have to deal with some quirks like passing objects around but still - this is making the idea of having a central configuration system so much more possible!&lt;/p&gt;
&lt;p&gt;Our clojurescript plugin could literally change how every other package reacts: how it is set up, what keybindings it uses, what options it needs and more. We could even change how other plugins look inside the editor by manipulating it&amp;rsquo;s DOM representation.&lt;/p&gt;
&lt;p&gt;Short said, in the last few days I started porting a bit of functionality from sublimious to atom.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./proton-demo.gif&#34; alt=&#34;demo&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;For sublimious, I did &lt;strong&gt;a lot of hacking&lt;/strong&gt;. I basically collected all the configuration and flushed it into real files that are then getting re-loaded by sublimetext. For atom, I don&amp;rsquo;t need to do that.&lt;/p&gt;
&lt;p&gt;How do I set a editor config? Like this!&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-clj&#34; data-lang=&#34;clj&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;defn &lt;/span&gt;set-config! [selector value]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  (&lt;span style=&#34;color:#a6e22e&#34;&gt;.set&lt;/span&gt; config selector value))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;How do I wipe the existing user config so it doesn&amp;rsquo;t conflict with our new one? One line.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-clj&#34; data-lang=&#34;clj&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(map atom-env/unset-config! (&lt;span style=&#34;color:#a6e22e&#34;&gt;atom-env/get-all-settings&lt;/span&gt;)))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The only hack I had to implement is the ability to install packages from within a package. That&amp;rsquo;s currently not possible. Here&amp;rsquo;s how I did it anyway:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-clj&#34; data-lang=&#34;clj&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;defn &lt;/span&gt;install-package [package-name]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  (&lt;span style=&#34;color:#66d9ef&#34;&gt;if &lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;is-installed?&lt;/span&gt; package-name)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    true
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    (&lt;span style=&#34;color:#a6e22e&#34;&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      (&lt;span style=&#34;color:#a6e22e&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        (&lt;span style=&#34;color:#a6e22e&#34;&gt;.execSync&lt;/span&gt; child-process (str (&lt;span style=&#34;color:#a6e22e&#34;&gt;get-apm-path&lt;/span&gt;) &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34; install &amp;#34;&lt;/span&gt; package-name &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34; --no-colors&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        true)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      (&lt;span style=&#34;color:#a6e22e&#34;&gt;catch&lt;/span&gt; js/Error e
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        false))))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(I&amp;rsquo;m just calling apm through node!)&lt;/p&gt;
&lt;h3 id=&#34;is-it-ready&#34;&gt;Is it ready?&lt;/h3&gt;
&lt;p&gt;Not even close to but I am already a lot more happy with it than with sublimious before. It feels more&amp;hellip; powerful! Maybe cljs+atom can really be a alternative to emacs at some point. Let me dream 😊&lt;/p&gt;
&lt;p&gt;In the meantime, if you want to help, please check it out &lt;a href=&#34;https://github.com/dvcrn/proton&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>If you read my last posts you might notice that I really like editors. Something about finding better and better tools is very interesting and fun. Plus using a new tool for a couple of weeks is keeping things fresh even when you work on the same codebase.</p>
<p>The problem is that since I started using <a href="http://dvcrn.github.io/2015/10/13/my_dream_editor.html">spacemacs</a>, I can&rsquo;t really use anything else. It just&hellip; doesn&rsquo;t work. The shortcuts are too awkward, get overriden by third party plugins and all the manual config orchestration is just a pain.</p>
<p>When I went back to Sublime Text for a little, I ended up writing <a href="https://github.com/dvcrn/sublimious">sublimious</a>, a plugin which ports a big chunk of the spacemacs toolset into Sublime Text:</p>
<p>
<img class="img-fluid" src="./MqsB6Pt.gif" alt="sublimious" />

</p>
<p>It works <strong>really REALLY good</strong>! But eventually I ran into problems implementing more things. Stuff like relative line numbers is close to impossible to do without a big performance hit or awkward behavior. Or the gutter still only being able to hold 1 element in it. What&rsquo;s up with that?</p>
<p>Performance wise it is crazy fast but let&rsquo;s face it: Sublime Text is dead even if the community doesn&rsquo;t want to admit it. Waiting until John Skinner to come along and commit a new feature once a year is just taking too long.</p>
<h3 id="my-first-time-with-atom">My first time with Atom?</h3>
<p>Now I am a little bit biased towards atom. I looked at it multiple times but it felt too slow, too in-performant and most of the plugins were just trash-level (sorry if I insulted some of my readers but they are just&hellip; not good!).</p>
<p>All the problems I have about plugins overwriting keybindings are here stronger than anywhere else. A plugin can literally change every little aspect of the editor. This is a really great thing, don&rsquo;t get me wrong! But at the same time you need to take double care of what you are installing.</p>
<p>Github also did a lot of weird design decicions that I can&rsquo;t follow. Here are a few:</p>
<h5 id="emoji-as-commit-messages">Emoji as commit messages</h5>
<p>I tried to nibble my way through the codebase and recent changes a bit but emojis&hellip; man. I mean, it&rsquo;s cool that you have fun with it but for outsiders it&rsquo;s unnecessary confusing. 🎨 is probably style related, I get that, but what the heck is 📝? 🔥?</p>
<p>Plus using emojis is not a substitute for a commit message. It&rsquo;s like me commiting with the word &ldquo;style.&rdquo;</p>
<h5 id="coffeescript">Coffeescript</h5>
<p>I used to like coffeescript a lot but reading through the atom sourcecode made me realise why I will probably not touch it again.</p>
<p>Take this snippet for example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-coffee" data-lang="coffee"><span style="display:flex;"><span><span style="color:#a6e22e">@eventElement</span>.<span style="color:#a6e22e">dispatchEvent</span>(<span style="color:#66d9ef">new</span> <span style="color:#a6e22e">CustomEvent</span>(<span style="color:#a6e22e">name</span>, bubbles: <span style="color:#66d9ef">true</span>, cancelable: <span style="color:#66d9ef">true</span>))
</span></span></code></pre></div><p>What is it doing? Why do some of the arguments have colons? Where does the object start and where does it end? The constructor is getting 3 arguments, right? Right?</p>
<p>I literally had to run this through a coffeescript parser to understand what it&rsquo;s doing:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">eventElement</span>.<span style="color:#a6e22e">dispatchEvent</span>(
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">CustomEvent</span>(<span style="color:#a6e22e">name</span>, {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">bubbles</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">cancelable</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>  }),
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><h5 id="confusing-objects-everywhere">confusing objects everywhere</h5>
<p>I&rsquo;m a big functional guy and everywhere I go in the atom sourcecode, I am dealing with objects after objects. I can&rsquo;t count how many <code>@element</code> assignments and reads I saw. Some things are getting assigned at the top of an object, used at the bottom and changed 3x until it reached that spot.</p>
<h5 id="cson">cson</h5>
<p>For some reason someone decided that coffeescript is so great that we should also start writing json in it! Because&hellip; commas are evil or something?</p>
<p>I quickly ran away from it</p>
<h3 id="atom-meet-clojurescript">Atom, meet clojurescript</h3>
<p>Now, since then and now I got a lot better in clojurescript so I decided to give it another go. I created a little <a href="https://github.com/dvcrn/ajom">lein template</a> for creating atom plugins in clojurescript and hacked a bit here and there.</p>
<p><strong>Turns out this works really well!</strong> Clojurescript and atom are like&hellip; emacs and elisp! You have all the power of clojurescript combined with all the customisation options atom gives you. You still have to deal with some quirks like passing objects around but still - this is making the idea of having a central configuration system so much more possible!</p>
<p>Our clojurescript plugin could literally change how every other package reacts: how it is set up, what keybindings it uses, what options it needs and more. We could even change how other plugins look inside the editor by manipulating it&rsquo;s DOM representation.</p>
<p>Short said, in the last few days I started porting a bit of functionality from sublimious to atom.</p>
<p>
<img class="img-fluid" src="./proton-demo.gif" alt="demo" />

</p>
<p>For sublimious, I did <strong>a lot of hacking</strong>. I basically collected all the configuration and flushed it into real files that are then getting re-loaded by sublimetext. For atom, I don&rsquo;t need to do that.</p>
<p>How do I set a editor config? Like this!</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-clj" data-lang="clj"><span style="display:flex;"><span>(<span style="color:#66d9ef">defn </span>set-config! [selector value]
</span></span><span style="display:flex;"><span>  (<span style="color:#a6e22e">.set</span> config selector value))
</span></span></code></pre></div><p>How do I wipe the existing user config so it doesn&rsquo;t conflict with our new one? One line.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-clj" data-lang="clj"><span style="display:flex;"><span>(map atom-env/unset-config! (<span style="color:#a6e22e">atom-env/get-all-settings</span>)))
</span></span></code></pre></div><p>The only hack I had to implement is the ability to install packages from within a package. That&rsquo;s currently not possible. Here&rsquo;s how I did it anyway:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-clj" data-lang="clj"><span style="display:flex;"><span>(<span style="color:#66d9ef">defn </span>install-package [package-name]
</span></span><span style="display:flex;"><span>  (<span style="color:#66d9ef">if </span>(<span style="color:#a6e22e">is-installed?</span> package-name)
</span></span><span style="display:flex;"><span>    true
</span></span><span style="display:flex;"><span>    (<span style="color:#a6e22e">try</span>
</span></span><span style="display:flex;"><span>      (<span style="color:#a6e22e">do</span>
</span></span><span style="display:flex;"><span>        (<span style="color:#a6e22e">.execSync</span> child-process (str (<span style="color:#a6e22e">get-apm-path</span>) <span style="color:#e6db74">&#34; install &#34;</span> package-name <span style="color:#e6db74">&#34; --no-colors&#34;</span>))
</span></span><span style="display:flex;"><span>        true)
</span></span><span style="display:flex;"><span>      (<span style="color:#a6e22e">catch</span> js/Error e
</span></span><span style="display:flex;"><span>        false))))
</span></span></code></pre></div><p>(I&rsquo;m just calling apm through node!)</p>
<h3 id="is-it-ready">Is it ready?</h3>
<p>Not even close to but I am already a lot more happy with it than with sublimious before. It feels more&hellip; powerful! Maybe cljs+atom can really be a alternative to emacs at some point. Let me dream 😊</p>
<p>In the meantime, if you want to help, please check it out <a href="https://github.com/dvcrn/proton">here</a>.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Demand driven iOS apps with om and react native</title>
      <link>https://david.coffee/demand-driven-ios-apps-with-om-and-react-native/</link>
      <pubDate>Thu, 19 Nov 2015 11:01:23 +0000</pubDate>
      
      <guid>https://david.coffee/demand-driven-ios-apps-with-om-and-react-native/</guid><description>&lt;p&gt;I recently talked a lot about om and react native. Since people seemed quite interested in this topic, I decided to hold a little presentation at the last Tokyo iOS meetup in November.&lt;/p&gt;
&lt;p&gt;People were a bit overwhelmed (many never saw clojurescript before) but the general feedback was quite good. I am sure I convinced at least 1 person to give it a spin!&lt;/p&gt;
&lt;p&gt;In any way, here is my talk from that iOS meetup. Slides are available &lt;a href=&#34;http://www.slideshare.net/dvcrn/demand-driven-applications-with-omnext-and-react-native-55185632&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;div class=&#34;video-wrapper&#34;&gt;
&lt;iframe width=&#34;560&#34; height=&#34;315&#34; src=&#34;https://www.youtube.com/embed/oJ8t8Hc9XaE&#34; frameborder=&#34;0&#34; allowfullscreen&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;This was also the first time we decided to record things so excuse the mediocore quality. I experimented by putting a external microphone on the table next to me but didn&amp;rsquo;t realise that the macbook fan blowed straight into it. Still slightly better than the in-camera mic but still room for improvements.&lt;/p&gt;
&lt;p&gt;I for my part am very excited about clojurescript on native and super happy to be part of this still young but growing community. Clojurescript will come for you - if you want it or not. Better start learning it now! 😈&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>I recently talked a lot about om and react native. Since people seemed quite interested in this topic, I decided to hold a little presentation at the last Tokyo iOS meetup in November.</p>
<p>People were a bit overwhelmed (many never saw clojurescript before) but the general feedback was quite good. I am sure I convinced at least 1 person to give it a spin!</p>
<p>In any way, here is my talk from that iOS meetup. Slides are available <a href="http://www.slideshare.net/dvcrn/demand-driven-applications-with-omnext-and-react-native-55185632">here</a>.</p>
<div class="video-wrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/oJ8t8Hc9XaE" frameborder="0" allowfullscreen></iframe>
</div>
<p>This was also the first time we decided to record things so excuse the mediocore quality. I experimented by putting a external microphone on the table next to me but didn&rsquo;t realise that the macbook fan blowed straight into it. Still slightly better than the in-camera mic but still room for improvements.</p>
<p>I for my part am very excited about clojurescript on native and super happy to be part of this still young but growing community. Clojurescript will come for you - if you want it or not. Better start learning it now! 😈</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Going native with om.next</title>
      <link>https://david.coffee/going-native-with-om.next/</link>
      <pubDate>Tue, 27 Oct 2015 12:47:46 +0000</pubDate>
      
      <guid>https://david.coffee/going-native-with-om.next/</guid><description>&lt;p&gt;I mentioned already earlier that I believe &lt;a href=&#34;https://github.com/omcljs/om&#34;&gt;om.next&lt;/a&gt; will be the future. Not particular om itself but the idea it implements: demand driven applications. The component declares what data it needs and the server is just there for delivering it. Components don&amp;rsquo;t want to coordinate 20 ajax requests just to gather all the data they need to operate. Components also don&amp;rsquo;t need all of the data your API provides in most cases, so why even the need to download all of it?&lt;/p&gt;
&lt;p&gt;These concepts make sense as the next iteration of server / client architectures but you know where they fit even better? &lt;strong&gt;Mobile Apps!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Especially when load time is critical and data volume limited, downloading big chunks of REST json data can get very annoying. We can&amp;rsquo;t just pump all the jsons our website uses into an app and expect it to be as performant as the web counterpart.&lt;/p&gt;
&lt;p&gt;So, wouldn&amp;rsquo;t it be amazing if our app just says &lt;em&gt;&amp;ldquo;I need the list of available books, but only the title and the image url and nothing else&amp;rdquo;&lt;/em&gt; and it will get exactly that and nothing more?&lt;/p&gt;
&lt;p&gt;With &lt;a href=&#34;https://github.com/omcljs/om&#34;&gt;next&lt;/a&gt; around the corner I decided to give it a try with react native. &lt;a href=&#34;http://cljsrn.org/&#34;&gt;cljsrn&lt;/a&gt; is already bundling a good amount of resources around the topic clojurescript and react native and &lt;a href=&#34;https://github.com/dmotz/natal&#34;&gt;natal&lt;/a&gt; and &lt;a href=&#34;http://cljsrn.org/ambly.html&#34;&gt;ambly&lt;/a&gt; already work great for legacy om, so let&amp;rsquo;s make it work for om.next!&lt;/p&gt;
&lt;h3 id=&#34;a-stoney-path&#34;&gt;A stoney path&lt;/h3&gt;
&lt;p&gt;After poking a bit around with next and natal, I quickly ran into a deadend because of it&amp;rsquo;s dependency to &lt;code&gt;gdom&lt;/code&gt; and &lt;code&gt;ReactDOM&lt;/code&gt;. Instead of calling &lt;code&gt;React.render&lt;/code&gt;, om.next relies on React 0.14 which splits the rendering logic from the component. Here we&amp;rsquo;re calling &lt;code&gt;ReactDOM.render&lt;/code&gt; instead, which, well, relies on a DOM.&lt;/p&gt;
&lt;p&gt;Luckily after a short chat with &lt;a href=&#34;https://github.com/swannodette&#34;&gt;@dnolen&lt;/a&gt; he promptly stripped out &lt;code&gt;ReactDOM&lt;/code&gt; (and &lt;code&gt;gdom&lt;/code&gt; as well) and added functionality that allows us to pass a &lt;code&gt;:root-render&lt;/code&gt; and &lt;code&gt;:root-unmount&lt;/code&gt; function into the reconciler. Whatever we specify here will get used to render and unmount components.&lt;/p&gt;
&lt;p&gt;Reading through the &lt;a href=&#34;https://github.com/facebook/react-native/tree/master/Libraries/ReactNative&#34;&gt;react native source&lt;/a&gt; made it clear that 0.14 has not been adapted yet and the rendering and unmounting logic is still inside &lt;code&gt;React&lt;/code&gt;. To plug this into om.next, all we have to do is&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-cljs&#34; data-lang=&#34;cljs&#34;&gt;:root-render #(.render js/React %1 %2)
:root-unmount #(.unmountComponentAtNode js/React %)})
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;em&gt;&amp;hellip;where js/React is the required react-native library.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Another thing that becomes clear is that React Native doesn&amp;rsquo;t render components into DOM references. Instead, it &lt;a href=&#34;https://github.com/facebook/react-native/blob/master/Libraries/ReactNative/ReactNative.js#L74-L80&#34;&gt;uses a number&lt;/a&gt; that references back to a component. With the first one being &lt;code&gt;1&lt;/code&gt;, we know exactly what where we have to add our root to:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-cljs&#34; data-lang=&#34;cljs&#34;&gt;(om/add-root! reconciler my-component 1)
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;wrapping-up&#34;&gt;Wrapping up&lt;/h3&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./om.next-native.png&#34; alt=&#34;showcase&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;The full code looks something like this:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-cljs&#34; data-lang=&#34;cljs&#34;&gt;;; Need to set js/React first so that Om can load
(set! js/React (js/require &amp;#34;react-native/Libraries/react-native/react-native.js&amp;#34;))

(ns future-next.core
  (:require [om.next :as om :refer-macros [defui]]))

;; Reset js/React back as the form above loads in an different React
(set! js/React (js/require &amp;#34;react-native/Libraries/react-native/react-native.js&amp;#34;))

;; Setup some methods to help create React Native elements
(defn view [opts &amp;amp; children]
  (apply js/React.createElement js/React.View (clj-&amp;gt;js opts) children))

(defn text [opts &amp;amp; children]
  (apply js/React.createElement js/React.Text (clj-&amp;gt;js opts) children))


;; Set up our Om UI
(defonce app-state (atom {:app/msg &amp;#34;Hello om.next!&amp;#34;}))

(defui WidgetComponent
  static om/IQuery
  (query [this]
         &amp;#39;[:app/msg])
  Object
  (render [this]
          (let [{:keys [app/msg]} (om/props this)]
            (view {:style {:flexDirection &amp;#34;column&amp;#34; :margin 40}}
                  (text nil msg))
            )))

 (defn read
   [{:keys [state] :as env} key params]
   (let [st @state]
     (if-let [[_ v] (find st key)]
       {:value v}
       {:value :not-found})))

 (def reconciler
   (om/reconciler
    {:state app-state
     :parser (om/parser {:read read})
     :root-render #(.render js/React %1 %2)
     :root-unmount #(.unmountComponentAtNode js/React %)})
   )

(om/add-root! reconciler WidgetComponent 1)

(defn ^:export init []
  ((fn render []
     (.requestAnimationFrame js/window render))))
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href=&#34;https://github.com/dmotz/natal/pull/18&#34;&gt;I submitted a PR&lt;/a&gt; to natal and once that&amp;rsquo;s merged in, all you have to do is &lt;code&gt;natal init&lt;/code&gt; :)&lt;/p&gt;
&lt;p&gt;Can you believe that all of this happened in just 1.5 days?&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>I mentioned already earlier that I believe <a href="https://github.com/omcljs/om">om.next</a> will be the future. Not particular om itself but the idea it implements: demand driven applications. The component declares what data it needs and the server is just there for delivering it. Components don&rsquo;t want to coordinate 20 ajax requests just to gather all the data they need to operate. Components also don&rsquo;t need all of the data your API provides in most cases, so why even the need to download all of it?</p>
<p>These concepts make sense as the next iteration of server / client architectures but you know where they fit even better? <strong>Mobile Apps!</strong></p>
<p>Especially when load time is critical and data volume limited, downloading big chunks of REST json data can get very annoying. We can&rsquo;t just pump all the jsons our website uses into an app and expect it to be as performant as the web counterpart.</p>
<p>So, wouldn&rsquo;t it be amazing if our app just says <em>&ldquo;I need the list of available books, but only the title and the image url and nothing else&rdquo;</em> and it will get exactly that and nothing more?</p>
<p>With <a href="https://github.com/omcljs/om">next</a> around the corner I decided to give it a try with react native. <a href="http://cljsrn.org/">cljsrn</a> is already bundling a good amount of resources around the topic clojurescript and react native and <a href="https://github.com/dmotz/natal">natal</a> and <a href="http://cljsrn.org/ambly.html">ambly</a> already work great for legacy om, so let&rsquo;s make it work for om.next!</p>
<h3 id="a-stoney-path">A stoney path</h3>
<p>After poking a bit around with next and natal, I quickly ran into a deadend because of it&rsquo;s dependency to <code>gdom</code> and <code>ReactDOM</code>. Instead of calling <code>React.render</code>, om.next relies on React 0.14 which splits the rendering logic from the component. Here we&rsquo;re calling <code>ReactDOM.render</code> instead, which, well, relies on a DOM.</p>
<p>Luckily after a short chat with <a href="https://github.com/swannodette">@dnolen</a> he promptly stripped out <code>ReactDOM</code> (and <code>gdom</code> as well) and added functionality that allows us to pass a <code>:root-render</code> and <code>:root-unmount</code> function into the reconciler. Whatever we specify here will get used to render and unmount components.</p>
<p>Reading through the <a href="https://github.com/facebook/react-native/tree/master/Libraries/ReactNative">react native source</a> made it clear that 0.14 has not been adapted yet and the rendering and unmounting logic is still inside <code>React</code>. To plug this into om.next, all we have to do is</p>
<pre tabindex="0"><code class="language-cljs" data-lang="cljs">:root-render #(.render js/React %1 %2)
:root-unmount #(.unmountComponentAtNode js/React %)})
</code></pre><p><em>&hellip;where js/React is the required react-native library.</em></p>
<p>Another thing that becomes clear is that React Native doesn&rsquo;t render components into DOM references. Instead, it <a href="https://github.com/facebook/react-native/blob/master/Libraries/ReactNative/ReactNative.js#L74-L80">uses a number</a> that references back to a component. With the first one being <code>1</code>, we know exactly what where we have to add our root to:</p>
<pre tabindex="0"><code class="language-cljs" data-lang="cljs">(om/add-root! reconciler my-component 1)
</code></pre><h3 id="wrapping-up">Wrapping up</h3>
<p>
<img class="img-fluid" src="./om.next-native.png" alt="showcase" />

</p>
<p>The full code looks something like this:</p>
<pre tabindex="0"><code class="language-cljs" data-lang="cljs">;; Need to set js/React first so that Om can load
(set! js/React (js/require &#34;react-native/Libraries/react-native/react-native.js&#34;))

(ns future-next.core
  (:require [om.next :as om :refer-macros [defui]]))

;; Reset js/React back as the form above loads in an different React
(set! js/React (js/require &#34;react-native/Libraries/react-native/react-native.js&#34;))

;; Setup some methods to help create React Native elements
(defn view [opts &amp; children]
  (apply js/React.createElement js/React.View (clj-&gt;js opts) children))

(defn text [opts &amp; children]
  (apply js/React.createElement js/React.Text (clj-&gt;js opts) children))


;; Set up our Om UI
(defonce app-state (atom {:app/msg &#34;Hello om.next!&#34;}))

(defui WidgetComponent
  static om/IQuery
  (query [this]
         &#39;[:app/msg])
  Object
  (render [this]
          (let [{:keys [app/msg]} (om/props this)]
            (view {:style {:flexDirection &#34;column&#34; :margin 40}}
                  (text nil msg))
            )))

 (defn read
   [{:keys [state] :as env} key params]
   (let [st @state]
     (if-let [[_ v] (find st key)]
       {:value v}
       {:value :not-found})))

 (def reconciler
   (om/reconciler
    {:state app-state
     :parser (om/parser {:read read})
     :root-render #(.render js/React %1 %2)
     :root-unmount #(.unmountComponentAtNode js/React %)})
   )

(om/add-root! reconciler WidgetComponent 1)

(defn ^:export init []
  ((fn render []
     (.requestAnimationFrame js/window render))))
</code></pre><p><a href="https://github.com/dmotz/natal/pull/18">I submitted a PR</a> to natal and once that&rsquo;s merged in, all you have to do is <code>natal init</code> :)</p>
<p>Can you believe that all of this happened in just 1.5 days?</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>MarkRight - a markdown editor in om&#43;electron</title>
      <link>https://david.coffee/markright-a-markdown-editor-in-om-electron/</link>
      <pubDate>Thu, 22 Oct 2015 19:43:17 +0900</pubDate>
      
      <guid>https://david.coffee/markright-a-markdown-editor-in-om-electron/</guid><description>&lt;p&gt;If you follow clojurescript development you might have already heard about the next iteration of om, &lt;a href=&#34;https://github.com/omcljs/om&#34;&gt;om.next&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;om.next is David Nolens attempt to bring together what big companies like facebook with &lt;a href=&#34;https://facebook.github.io/relay/&#34;&gt;relay&lt;/a&gt; and netflix with &lt;a href=&#34;https://facebook.github.io/relay/&#34;&gt;falcor&lt;/a&gt; are doing at the moment. I will not go into detail but instead of REST, it implements a architecture in which components clearly declare what kind of data they need to operate. A server / router is then gathering that data and passing it back. Instead of multiple REST endpoints, you&amp;rsquo;ll end up with 1 graph endpoint that understands the client query.&lt;/p&gt;
&lt;p&gt;While still in early alpha, I was looking for an excuse to play around with it - or at least a excuse to use it in the most simple case possible. After scratching my head for a bit I came up with a &lt;strong&gt;markdown editor&lt;/strong&gt;. But why the hell would I want to write a markdown editor?&lt;/p&gt;
&lt;p&gt;When I have to write markdown, I&amp;rsquo;m missing a perfect live preview. I know, there are options out there that give you that either webbased or stuff that just let&amp;rsquo;s you preview what you wrote. The most popular probably being &lt;a href=&#34;http://25.io/mou/&#34;&gt;mou&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But I don&amp;rsquo;t like mou and it looks like they seem to charge for it soon. On top of that, I was eyeballing githubs &lt;a href=&#34;https://github.com/atom/electron&#34;&gt;electron&lt;/a&gt; (the thing that atom uses) for a while and wanted to give it a try.&lt;/p&gt;
&lt;h2 id=&#34;meet-markright&#34;&gt;Meet MarkRight&lt;/h2&gt;
&lt;p&gt;So I played a bit electron and om, created a pull request to &lt;a href=&#34;https://github.com/karad/lein_template_descjop/&#34;&gt;descjop&lt;/a&gt; on the way and eventually came up with &lt;strong&gt;MarkRight&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;https://raw.githubusercontent.com/dvcrn/dmedit/master/resources/markright-banner.png&#34; alt=&#34;markright banner&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;Markright is like the most simple editor you can imagine but it is this simplicity that I love! It is literally powered by 2 (or 3 if you count root) om / react components and all they do is piping your input text in real-time through &lt;a href=&#34;https://github.com/chjj/marked&#34;&gt;marked&lt;/a&gt;. It was fun to give &lt;a href=&#34;https://codemirror.net/&#34;&gt;codemirror&lt;/a&gt; a spin as well!&lt;/p&gt;
&lt;p&gt;I was really surprised how easy it was to just plug these different components together. The open source community did an amazing job with building and maintaining these cool toys!&lt;/p&gt;
&lt;p&gt;Anyway, here&amp;rsquo;s how it looks:&lt;/p&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;https://david.coffee/_Files/images/markright.png&#34; alt=&#34;screenshot&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s not much to it yet. You can save files, open files and - well - write markdown. There&amp;rsquo;s a lot of things to do but if you like the idea, please feel free to &lt;a href=&#34;https://github.com/dvcrn/markright&#34;&gt;contribute at github&lt;/a&gt; :)&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>If you follow clojurescript development you might have already heard about the next iteration of om, <a href="https://github.com/omcljs/om">om.next</a>.</p>
<p>om.next is David Nolens attempt to bring together what big companies like facebook with <a href="https://facebook.github.io/relay/">relay</a> and netflix with <a href="https://facebook.github.io/relay/">falcor</a> are doing at the moment. I will not go into detail but instead of REST, it implements a architecture in which components clearly declare what kind of data they need to operate. A server / router is then gathering that data and passing it back. Instead of multiple REST endpoints, you&rsquo;ll end up with 1 graph endpoint that understands the client query.</p>
<p>While still in early alpha, I was looking for an excuse to play around with it - or at least a excuse to use it in the most simple case possible. After scratching my head for a bit I came up with a <strong>markdown editor</strong>. But why the hell would I want to write a markdown editor?</p>
<p>When I have to write markdown, I&rsquo;m missing a perfect live preview. I know, there are options out there that give you that either webbased or stuff that just let&rsquo;s you preview what you wrote. The most popular probably being <a href="http://25.io/mou/">mou</a>.</p>
<p>But I don&rsquo;t like mou and it looks like they seem to charge for it soon. On top of that, I was eyeballing githubs <a href="https://github.com/atom/electron">electron</a> (the thing that atom uses) for a while and wanted to give it a try.</p>
<h2 id="meet-markright">Meet MarkRight</h2>
<p>So I played a bit electron and om, created a pull request to <a href="https://github.com/karad/lein_template_descjop/">descjop</a> on the way and eventually came up with <strong>MarkRight</strong>.</p>
<p>
<img class="img-fluid" src="https://raw.githubusercontent.com/dvcrn/dmedit/master/resources/markright-banner.png" alt="markright banner" />

</p>
<p>Markright is like the most simple editor you can imagine but it is this simplicity that I love! It is literally powered by 2 (or 3 if you count root) om / react components and all they do is piping your input text in real-time through <a href="https://github.com/chjj/marked">marked</a>. It was fun to give <a href="https://codemirror.net/">codemirror</a> a spin as well!</p>
<p>I was really surprised how easy it was to just plug these different components together. The open source community did an amazing job with building and maintaining these cool toys!</p>
<p>Anyway, here&rsquo;s how it looks:</p>
<p>
<img class="img-fluid" src="https://david.coffee/_Files/images/markright.png" alt="screenshot" />

</p>
<p>There&rsquo;s not much to it yet. You can save files, open files and - well - write markdown. There&rsquo;s a lot of things to do but if you like the idea, please feel free to <a href="https://github.com/dvcrn/markright">contribute at github</a> :)</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>My Dream Editor</title>
      <link>https://david.coffee/my-dream-editor/</link>
      <pubDate>Tue, 13 Oct 2015 16:05:17 +0900</pubDate>
      
      <guid>https://david.coffee/my-dream-editor/</guid><description>&lt;p&gt;Today, let&amp;rsquo;s talk about editors. Why? Because currently there are more editors than anything else. Microsoft decided to launch an editor, github decided to launch an editor - heck, even facebook suddenly invests into editors. We have closed source ones, open source ones, big full powered IDEs and of course the old school family.&lt;/p&gt;
&lt;p&gt;The good thing, I actually like playing with editors. Sometimes I feel like just opening a new tool and test it out. Because of that I discovered SublimeText from NetBeans / PHPStorm. Because of that I also switched from SublimeText to VIM and later back to intellij. I love to constantly improve my workflow and find new ways to get even more productive.&lt;/p&gt;
&lt;p&gt;The annoying thing about testing editors is that well, once you hit VIM you are pretty much in a dead end. From then on each and every editor &lt;strong&gt;has&lt;/strong&gt; to have VIM keybindings. If it doesn&amp;rsquo;t, you can&amp;rsquo;t use it. Naturally I spent a ton of time with VIM - the best vim emulation out there. SublimeText and Atom just couldn&amp;rsquo;t cut it. They add a good amount of sugar into the mix, sugar I really like - but their VIM plugins are just not good enough (yet). So I kept going my way with vim, carefully tweaking my good old &lt;code&gt;.vimrc&lt;/code&gt; as days come by.&lt;/p&gt;
&lt;p&gt;To my big surprise, the editor that won me by storm wasn&amp;rsquo;t vim and it wasn&amp;rsquo;t neovim either. It was something that made me drop my beloved .vimrc without hesitation, something I thought I would never even touch - emacs. Yeah, emacs made me drop all other editors in the year 2015. Ridiculous, right? So what&amp;rsquo;s going on?&lt;/p&gt;
&lt;h1 id=&#34;enter-spacemacs&#34;&gt;Enter spacemacs&lt;/h1&gt;
&lt;p&gt;
&lt;img class=&#34;img-fluid&#34; src=&#34;./spacemacs-python.png&#34; alt=&#34;spacemacs&#34; /&gt;

&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/syl20bnr/spacemacs/&#34;&gt;spacemacs&lt;/a&gt; is an amazing pre-configured emacs made for people like me! VIM users!. It uses the power of evil, the arguably best VIM emulation out there and builds a layer of highly productive keybindings on top. Here&amp;rsquo;s how it works:&lt;/p&gt;
&lt;p&gt;In spacemacs, each command is triggered by the spacebar and follows a very innovative mnemonic. &lt;code&gt;spc&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; for example is showing you actions on the current buffer. &lt;code&gt;spc&lt;/code&gt; and &lt;code&gt;p&lt;/code&gt; executes project commands.&lt;/p&gt;
&lt;p&gt;The really cool thing though comes from it&amp;rsquo;s layer system. spacemacs has a ton of &lt;a href=&#34;https://github.com/syl20bnr/spacemacs/tree/master/layers&#34;&gt;contributed layers&lt;/a&gt; that &lt;em&gt;just work&lt;/em&gt;. For example without researching anything, I just enabled the react layer and suddenly I had full jsx support, highlighting, linting with eslint and a ton of keybindings! Just like that! Why? Because someone who actually uses react implemented it this way, the way it should be done.&lt;/p&gt;
&lt;p&gt;So many &lt;em&gt;strong&lt;/em&gt; editors give me a ton plugins and what not without actually knowing the ecosystem of the target language / framework just for the sake of supporting as many things as possible.&lt;/p&gt;
&lt;p&gt;And &lt;em&gt;if&lt;/em&gt; the plugin is superb, then the base of the editor is usually weak. Look at VIM: We have so many cool plugins but most of them are a big ugly hack in vimscript that works so-so. Emacs on the other hand is hackable in every little corner. It provides a powerful scripting language in form of &lt;code&gt;elisp&lt;/code&gt; and has a very active community around it.&lt;/p&gt;
&lt;p&gt;I would even argue that most of the layers that ship &lt;strong&gt;with&lt;/strong&gt; spacemacs out of the box are a good amount better than what I hacked together inside my .vimrc.&lt;/p&gt;
&lt;p&gt;I was so impressed by it that I decided to &lt;a href=&#34;https://github.com/dvcrn/sublimious&#34;&gt;port parts of it to sublimetext&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My lovestory with spacemacs begins. Where were you all my life, you beauty of an editor?&lt;/p&gt;
&lt;h1 id=&#34;what-else&#34;&gt;What else?&lt;/h1&gt;
&lt;p&gt;Here&amp;rsquo;s my &amp;lsquo;mini ranking&amp;rsquo; of editors I like:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/syl20bnr/spacemacs/&#34;&gt;spacemacs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.jetbrains.com/idea/&#34;&gt;intellij&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;vim&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;http://www.sublimetext.com/&#34;&gt;sublimetext&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://atom.io/&#34;&gt;atom&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
      <content:encoded><![CDATA[<p>Today, let&rsquo;s talk about editors. Why? Because currently there are more editors than anything else. Microsoft decided to launch an editor, github decided to launch an editor - heck, even facebook suddenly invests into editors. We have closed source ones, open source ones, big full powered IDEs and of course the old school family.</p>
<p>The good thing, I actually like playing with editors. Sometimes I feel like just opening a new tool and test it out. Because of that I discovered SublimeText from NetBeans / PHPStorm. Because of that I also switched from SublimeText to VIM and later back to intellij. I love to constantly improve my workflow and find new ways to get even more productive.</p>
<p>The annoying thing about testing editors is that well, once you hit VIM you are pretty much in a dead end. From then on each and every editor <strong>has</strong> to have VIM keybindings. If it doesn&rsquo;t, you can&rsquo;t use it. Naturally I spent a ton of time with VIM - the best vim emulation out there. SublimeText and Atom just couldn&rsquo;t cut it. They add a good amount of sugar into the mix, sugar I really like - but their VIM plugins are just not good enough (yet). So I kept going my way with vim, carefully tweaking my good old <code>.vimrc</code> as days come by.</p>
<p>To my big surprise, the editor that won me by storm wasn&rsquo;t vim and it wasn&rsquo;t neovim either. It was something that made me drop my beloved .vimrc without hesitation, something I thought I would never even touch - emacs. Yeah, emacs made me drop all other editors in the year 2015. Ridiculous, right? So what&rsquo;s going on?</p>
<h1 id="enter-spacemacs">Enter spacemacs</h1>
<p>
<img class="img-fluid" src="./spacemacs-python.png" alt="spacemacs" />

</p>
<p><a href="https://github.com/syl20bnr/spacemacs/">spacemacs</a> is an amazing pre-configured emacs made for people like me! VIM users!. It uses the power of evil, the arguably best VIM emulation out there and builds a layer of highly productive keybindings on top. Here&rsquo;s how it works:</p>
<p>In spacemacs, each command is triggered by the spacebar and follows a very innovative mnemonic. <code>spc</code> and <code>b</code> for example is showing you actions on the current buffer. <code>spc</code> and <code>p</code> executes project commands.</p>
<p>The really cool thing though comes from it&rsquo;s layer system. spacemacs has a ton of <a href="https://github.com/syl20bnr/spacemacs/tree/master/layers">contributed layers</a> that <em>just work</em>. For example without researching anything, I just enabled the react layer and suddenly I had full jsx support, highlighting, linting with eslint and a ton of keybindings! Just like that! Why? Because someone who actually uses react implemented it this way, the way it should be done.</p>
<p>So many <em>strong</em> editors give me a ton plugins and what not without actually knowing the ecosystem of the target language / framework just for the sake of supporting as many things as possible.</p>
<p>And <em>if</em> the plugin is superb, then the base of the editor is usually weak. Look at VIM: We have so many cool plugins but most of them are a big ugly hack in vimscript that works so-so. Emacs on the other hand is hackable in every little corner. It provides a powerful scripting language in form of <code>elisp</code> and has a very active community around it.</p>
<p>I would even argue that most of the layers that ship <strong>with</strong> spacemacs out of the box are a good amount better than what I hacked together inside my .vimrc.</p>
<p>I was so impressed by it that I decided to <a href="https://github.com/dvcrn/sublimious">port parts of it to sublimetext</a>.</p>
<p>My lovestory with spacemacs begins. Where were you all my life, you beauty of an editor?</p>
<h1 id="what-else">What else?</h1>
<p>Here&rsquo;s my &lsquo;mini ranking&rsquo; of editors I like:</p>
<ol>
<li><a href="https://github.com/syl20bnr/spacemacs/">spacemacs</a></li>
<li><a href="https://www.jetbrains.com/idea/">intellij</a></li>
<li>vim</li>
<li><a href="http://www.sublimetext.com/">sublimetext</a></li>
<li><a href="https://atom.io/">atom</a></li>
</ol>
]]></content:encoded>
    </item>
    
    <item>
      <title>A new blog</title>
      <link>https://david.coffee/a-new-blog/</link>
      <pubDate>Sun, 11 Oct 2015 11:39:46 +0000</pubDate>
      
      <guid>https://david.coffee/a-new-blog/</guid><description>&lt;p&gt;I decided to create another simple blog based on jekyll next to my &lt;a href=&#34;https://medium.com/@davidmohl&#34;&gt;medium one&lt;/a&gt;. Why? let me explain.&lt;/p&gt;
&lt;p&gt;Medium is an amazing platform for &lt;strong&gt;writing&lt;/strong&gt;. That&amp;rsquo;s what works really damn good on it. But writing doesn&amp;rsquo;t equal blog. Medium is a magazine - a collection of articles and columns from individual writers. When you see a post about &amp;lsquo;How to set up php&amp;rsquo; with a big editorial picture and a ton of quotes, don&amp;rsquo;t you also think that it kind of feels&amp;hellip; out of place?&lt;/p&gt;
&lt;p&gt;Medium will be the place where I&amp;rsquo;ll write thought through stuff. Stuff that could go into a magazine. On jekyll (here) on the other hand, I can write anything that is somehow tech related:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Some guide for a new technology? Here.&lt;/li&gt;
&lt;li&gt;A article on how to work productively? Medium.&lt;/li&gt;
&lt;li&gt;A post on workflow thoughts? Medium.&lt;/li&gt;
&lt;li&gt;Code snippets how to set up one of my projects? Here.&lt;/li&gt;
&lt;li&gt;My thoughts on a certain technology? Here.&lt;/li&gt;
&lt;li&gt;Common mistakes companies make? Medium.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;hellip; you get the drill right? And in general, if you subscribe through RSS it doesn&amp;rsquo;t really matter where the articles come from.&lt;/p&gt;
&lt;p&gt;Now there&amp;rsquo;s just the problem that I really don&amp;rsquo;t blog much in general&amp;hellip;&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>I decided to create another simple blog based on jekyll next to my <a href="https://medium.com/@davidmohl">medium one</a>. Why? let me explain.</p>
<p>Medium is an amazing platform for <strong>writing</strong>. That&rsquo;s what works really damn good on it. But writing doesn&rsquo;t equal blog. Medium is a magazine - a collection of articles and columns from individual writers. When you see a post about &lsquo;How to set up php&rsquo; with a big editorial picture and a ton of quotes, don&rsquo;t you also think that it kind of feels&hellip; out of place?</p>
<p>Medium will be the place where I&rsquo;ll write thought through stuff. Stuff that could go into a magazine. On jekyll (here) on the other hand, I can write anything that is somehow tech related:</p>
<ul>
<li>Some guide for a new technology? Here.</li>
<li>A article on how to work productively? Medium.</li>
<li>A post on workflow thoughts? Medium.</li>
<li>Code snippets how to set up one of my projects? Here.</li>
<li>My thoughts on a certain technology? Here.</li>
<li>Common mistakes companies make? Medium.</li>
</ul>
<p>&hellip; you get the drill right? And in general, if you subscribe through RSS it doesn&rsquo;t really matter where the articles come from.</p>
<p>Now there&rsquo;s just the problem that I really don&rsquo;t blog much in general&hellip;</p>
]]></content:encoded>
    </item>
    
    <item>
      <title></title>
      <link>https://david.coffee/about/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/about/</guid><description>&lt;h1 id=&#34;about&#34;&gt;About&lt;/h1&gt;
&lt;p&gt;Hi, I&amp;rsquo;m David! I&amp;rsquo;m an engineering leader and builder currently based in Tokyo 🇯🇵.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m an ex-engineering manager from Mercari with over 14 years of experience in the tech industry. I&amp;rsquo;m experienced in the full stack of application development and have helped businesses ship projects to millions of users. I have a deep passion for building strong, productive engineering teams and reducing friction in the development process.&lt;/p&gt;
&lt;p&gt;Today, I focus on indie hacking and building my own business under the name &lt;strong&gt;&lt;a href=&#34;https://digitalvibes.dev&#34;&gt;Digital Vibes&lt;/a&gt;&lt;/strong&gt;. I focus on shipping fast and experimenting with new ideas.&lt;/p&gt;
&lt;p&gt;You can &lt;strong&gt;&lt;a href=&#34;https://david.coffee/projects/&#34;&gt;see what I&amp;rsquo;m working on here&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&#34;what-im-building&#34;&gt;What I&amp;rsquo;m building&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;m currently maintaining and growing a suite of apps focused on productivity, AI, and developer experience:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&#34;https://configmesh.app&#34;&gt;ConfigMesh&lt;/a&gt;&lt;/strong&gt;: E2e encrypted sync for your dotfiles and app settings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&#34;https://maskwire.com&#34;&gt;Maskwire&lt;/a&gt;&lt;/strong&gt;: A transparent email proxy for power users.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&#34;https://fixmyjapanese.com&#34;&gt;Fix My Japanese&lt;/a&gt;&lt;/strong&gt;: AI-powered Japanese grammar correction for iOS and Web.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&#34;https://quickshot.photo&#34;&gt;Quickshot&lt;/a&gt;&lt;/strong&gt;: A concept photo editor powered by generative AI.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;my-process&#34;&gt;My Process&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;m a big believer in velocity and using AI to handle the grunt work. I recently wrote about my &lt;strong&gt;&lt;a href=&#34;https://david.coffee/move-fast-stack-2026/&#34;&gt;2026 &amp;ldquo;Move Fast&amp;rdquo; Stack&lt;/a&gt;&lt;/strong&gt; if you&amp;rsquo;re curious about the technical side of how I ship these projects.&lt;/p&gt;
&lt;h3 id=&#34;contact&#34;&gt;Contact&lt;/h3&gt;
&lt;p&gt;The best way to reach me is on &lt;strong&gt;&lt;a href=&#34;https://x.com/dvcrn&#34;&gt;Twitter/X&lt;/a&gt;&lt;/strong&gt; or &lt;strong&gt;&lt;a href=&#34;https://bsky.app/profile/david.d.sh&#34;&gt;Bluesky&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re interested in following my journey, consider &lt;strong&gt;&lt;a href=&#34;https://david.coffee/index.xml&#34;&gt;subscribing to the RSS feed&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<h1 id="about">About</h1>
<p>Hi, I&rsquo;m David! I&rsquo;m an engineering leader and builder currently based in Tokyo 🇯🇵.</p>
<p>I&rsquo;m an ex-engineering manager from Mercari with over 14 years of experience in the tech industry. I&rsquo;m experienced in the full stack of application development and have helped businesses ship projects to millions of users. I have a deep passion for building strong, productive engineering teams and reducing friction in the development process.</p>
<p>Today, I focus on indie hacking and building my own business under the name <strong><a href="https://digitalvibes.dev">Digital Vibes</a></strong>. I focus on shipping fast and experimenting with new ideas.</p>
<p>You can <strong><a href="https://david.coffee/projects/">see what I&rsquo;m working on here</a></strong>.</p>
<h3 id="what-im-building">What I&rsquo;m building</h3>
<p>I&rsquo;m currently maintaining and growing a suite of apps focused on productivity, AI, and developer experience:</p>
<ul>
<li><strong><a href="https://configmesh.app">ConfigMesh</a></strong>: E2e encrypted sync for your dotfiles and app settings.</li>
<li><strong><a href="https://maskwire.com">Maskwire</a></strong>: A transparent email proxy for power users.</li>
<li><strong><a href="https://fixmyjapanese.com">Fix My Japanese</a></strong>: AI-powered Japanese grammar correction for iOS and Web.</li>
<li><strong><a href="https://quickshot.photo">Quickshot</a></strong>: A concept photo editor powered by generative AI.</li>
</ul>
<h3 id="my-process">My Process</h3>
<p>I&rsquo;m a big believer in velocity and using AI to handle the grunt work. I recently wrote about my <strong><a href="https://david.coffee/move-fast-stack-2026/">2026 &ldquo;Move Fast&rdquo; Stack</a></strong> if you&rsquo;re curious about the technical side of how I ship these projects.</p>
<h3 id="contact">Contact</h3>
<p>The best way to reach me is on <strong><a href="https://x.com/dvcrn">Twitter/X</a></strong> or <strong><a href="https://bsky.app/profile/david.d.sh">Bluesky</a></strong>.</p>
<p>If you&rsquo;re interested in following my journey, consider <strong><a href="https://david.coffee/index.xml">subscribing to the RSS feed</a></strong>.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title></title>
      <link>https://david.coffee/projects/advancedaiactions/privacy/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/projects/advancedaiactions/privacy/</guid><description>&lt;h1 id=&#34;privacy-policy-for-advanced-ai-actions&#34;&gt;Privacy policy for Advanced AI Actions&lt;/h1&gt;
&lt;p&gt;This app does not collect any or personal information.&lt;/p&gt;
&lt;p&gt;No personal data or information is explicitly collected by this app beyond necessary operational data.&lt;/p&gt;
&lt;h2 id=&#34;third-party-services&#34;&gt;Third-Party Services&lt;/h2&gt;
&lt;p&gt;This app may utilize third-party services for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Error tracking and performance monitoring (like Sentry)&lt;/li&gt;
&lt;li&gt;Subscription management (like RevenueCat)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These services collect standard technical data according to their respective privacy policies.&lt;/p&gt;
&lt;h2 id=&#34;ai-processing&#34;&gt;AI Processing&lt;/h2&gt;
&lt;p&gt;When you use AI features in this app:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your content is sent to AI providers (such as Google, OpenAI, Anthropic, or OpenRouter by default)&lt;/li&gt;
&lt;li&gt;You can add and configure additional AI providers of your choice&lt;/li&gt;
&lt;li&gt;All content is subject to the privacy policies of the respective AI providers you use&lt;/li&gt;
&lt;li&gt;We do not store or reuse your prompts or generated content&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;security&#34;&gt;Security&lt;/h2&gt;
&lt;p&gt;All communications with AI providers use secure, encrypted connections.&lt;/p&gt;
&lt;h2 id=&#34;contact&#34;&gt;Contact&lt;/h2&gt;
&lt;p&gt;For questions about this privacy policy, please &lt;a href=&#34;mailto:contact@davidmohl.com&#34;&gt;contact me&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Last updated: 2025-03-12&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<h1 id="privacy-policy-for-advanced-ai-actions">Privacy policy for Advanced AI Actions</h1>
<p>This app does not collect any or personal information.</p>
<p>No personal data or information is explicitly collected by this app beyond necessary operational data.</p>
<h2 id="third-party-services">Third-Party Services</h2>
<p>This app may utilize third-party services for:</p>
<ul>
<li>Error tracking and performance monitoring (like Sentry)</li>
<li>Subscription management (like RevenueCat)</li>
</ul>
<p>These services collect standard technical data according to their respective privacy policies.</p>
<h2 id="ai-processing">AI Processing</h2>
<p>When you use AI features in this app:</p>
<ul>
<li>Your content is sent to AI providers (such as Google, OpenAI, Anthropic, or OpenRouter by default)</li>
<li>You can add and configure additional AI providers of your choice</li>
<li>All content is subject to the privacy policies of the respective AI providers you use</li>
<li>We do not store or reuse your prompts or generated content</li>
</ul>
<h2 id="security">Security</h2>
<p>All communications with AI providers use secure, encrypted connections.</p>
<h2 id="contact">Contact</h2>
<p>For questions about this privacy policy, please <a href="mailto:contact@davidmohl.com">contact me</a></p>
<p>Last updated: 2025-03-12</p>
]]></content:encoded>
    </item>
    
    <item>
      <title></title>
      <link>https://david.coffee/projects/quickshot/privacy/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/projects/quickshot/privacy/</guid><description>&lt;h1 id=&#34;privacy-policy-for-quickshot-ai-editor&#34;&gt;Privacy Policy for Quickshot AI Editor&lt;/h1&gt;
&lt;p&gt;Last updated: 2025-05-02&lt;/p&gt;
&lt;h2 id=&#34;overview&#34;&gt;Overview&lt;/h2&gt;
&lt;p&gt;Quickshot AI Editor (&amp;ldquo;the App&amp;rdquo;) is designed with your privacy in mind. This policy explains what information we collect, how we use it, and what choices you have regarding your data.&lt;/p&gt;
&lt;h2 id=&#34;information-we-collect&#34;&gt;Information We Collect&lt;/h2&gt;
&lt;h3 id=&#34;user-provided-information&#34;&gt;User-Provided Information&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Photos and Images&lt;/strong&gt;: Images you choose to edit using the App&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edit Descriptions&lt;/strong&gt;: Text descriptions you provide for editing your photos&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authentication Information&lt;/strong&gt;: When you sign in with Apple, we receive a unique identifier and your email address (if you choose to share it)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;automatically-collected-information&#34;&gt;Automatically Collected Information&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Usage Data&lt;/strong&gt;: Basic information about how you use the App, including edit sessions and feature usage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Device Information&lt;/strong&gt;: Technical data such as device type, operating system version, and App version&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error Data&lt;/strong&gt;: Information about App crashes and errors to improve stability&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;how-we-use-your-information&#34;&gt;How We Use Your Information&lt;/h2&gt;
&lt;h3 id=&#34;server-storage&#34;&gt;Server Storage&lt;/h3&gt;
&lt;p&gt;Our server stores:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Authentication tokens to maintain your session&lt;/li&gt;
&lt;li&gt;Basic usage metrics to improve the App&lt;/li&gt;
&lt;li&gt;Temporary copies of images during the editing process&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;ai-processing&#34;&gt;AI Processing&lt;/h3&gt;
&lt;p&gt;When you use the App&amp;rsquo;s editing features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your images and edit descriptions are sent to AI providers (such as Google or OpenAI)&lt;/li&gt;
&lt;li&gt;These transmissions are necessary to provide the core functionality of the App&lt;/li&gt;
&lt;li&gt;All content is subject to the privacy policies of the respective AI providers&lt;/li&gt;
&lt;li&gt;We do not permanently store your images or edit descriptions on our servers after processing is complete&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;third-party-services&#34;&gt;Third-Party Services&lt;/h2&gt;
&lt;p&gt;The App utilizes third-party services for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Authentication&lt;/strong&gt;: Sign in with Apple&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subscription Management&lt;/strong&gt;: RevenueCat for handling in-app purchases&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error Tracking&lt;/strong&gt;: Basic error and crash reporting tools&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These services collect standard technical data according to their respective privacy policies.&lt;/p&gt;
&lt;h2 id=&#34;data-retention&#34;&gt;Data Retention&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Edit history is stored locally on your device&lt;/li&gt;
&lt;li&gt;Server-side data is retained only as long as necessary to provide the service&lt;/li&gt;
&lt;li&gt;You can delete your account and associated data by contacting us&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;security&#34;&gt;Security&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;All communications between the App, our servers, and AI providers use secure, encrypted connections&lt;/li&gt;
&lt;li&gt;We implement reasonable security measures to protect your information&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;your-rights&#34;&gt;Your Rights&lt;/h2&gt;
&lt;p&gt;Depending on your location, you may have rights to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Access your personal information&lt;/li&gt;
&lt;li&gt;Correct inaccurate data&lt;/li&gt;
&lt;li&gt;Delete your data&lt;/li&gt;
&lt;li&gt;Object to certain processing activities&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;changes-to-this-policy&#34;&gt;Changes to This Policy&lt;/h2&gt;
&lt;p&gt;We may update this privacy policy from time to time. We will notify you of any changes by posting the new policy on this page and updating the &amp;ldquo;Last updated&amp;rdquo; date.&lt;/p&gt;
&lt;h2 id=&#34;contact&#34;&gt;Contact&lt;/h2&gt;
&lt;p&gt;For questions about this privacy policy, please &lt;a href=&#34;mailto:contact@davidmohl.com&#34;&gt;contact me&lt;/a&gt;&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<h1 id="privacy-policy-for-quickshot-ai-editor">Privacy Policy for Quickshot AI Editor</h1>
<p>Last updated: 2025-05-02</p>
<h2 id="overview">Overview</h2>
<p>Quickshot AI Editor (&ldquo;the App&rdquo;) is designed with your privacy in mind. This policy explains what information we collect, how we use it, and what choices you have regarding your data.</p>
<h2 id="information-we-collect">Information We Collect</h2>
<h3 id="user-provided-information">User-Provided Information</h3>
<ul>
<li><strong>Photos and Images</strong>: Images you choose to edit using the App</li>
<li><strong>Edit Descriptions</strong>: Text descriptions you provide for editing your photos</li>
<li><strong>Authentication Information</strong>: When you sign in with Apple, we receive a unique identifier and your email address (if you choose to share it)</li>
</ul>
<h3 id="automatically-collected-information">Automatically Collected Information</h3>
<ul>
<li><strong>Usage Data</strong>: Basic information about how you use the App, including edit sessions and feature usage</li>
<li><strong>Device Information</strong>: Technical data such as device type, operating system version, and App version</li>
<li><strong>Error Data</strong>: Information about App crashes and errors to improve stability</li>
</ul>
<h2 id="how-we-use-your-information">How We Use Your Information</h2>
<h3 id="server-storage">Server Storage</h3>
<p>Our server stores:</p>
<ul>
<li>Authentication tokens to maintain your session</li>
<li>Basic usage metrics to improve the App</li>
<li>Temporary copies of images during the editing process</li>
</ul>
<h3 id="ai-processing">AI Processing</h3>
<p>When you use the App&rsquo;s editing features:</p>
<ul>
<li>Your images and edit descriptions are sent to AI providers (such as Google or OpenAI)</li>
<li>These transmissions are necessary to provide the core functionality of the App</li>
<li>All content is subject to the privacy policies of the respective AI providers</li>
<li>We do not permanently store your images or edit descriptions on our servers after processing is complete</li>
</ul>
<h2 id="third-party-services">Third-Party Services</h2>
<p>The App utilizes third-party services for:</p>
<ul>
<li><strong>Authentication</strong>: Sign in with Apple</li>
<li><strong>Subscription Management</strong>: RevenueCat for handling in-app purchases</li>
<li><strong>Error Tracking</strong>: Basic error and crash reporting tools</li>
</ul>
<p>These services collect standard technical data according to their respective privacy policies.</p>
<h2 id="data-retention">Data Retention</h2>
<ul>
<li>Edit history is stored locally on your device</li>
<li>Server-side data is retained only as long as necessary to provide the service</li>
<li>You can delete your account and associated data by contacting us</li>
</ul>
<h2 id="security">Security</h2>
<ul>
<li>All communications between the App, our servers, and AI providers use secure, encrypted connections</li>
<li>We implement reasonable security measures to protect your information</li>
</ul>
<h2 id="your-rights">Your Rights</h2>
<p>Depending on your location, you may have rights to:</p>
<ul>
<li>Access your personal information</li>
<li>Correct inaccurate data</li>
<li>Delete your data</li>
<li>Object to certain processing activities</li>
</ul>
<h2 id="changes-to-this-policy">Changes to This Policy</h2>
<p>We may update this privacy policy from time to time. We will notify you of any changes by posting the new policy on this page and updating the &ldquo;Last updated&rdquo; date.</p>
<h2 id="contact">Contact</h2>
<p>For questions about this privacy policy, please <a href="mailto:contact@davidmohl.com">contact me</a></p>
]]></content:encoded>
    </item>
    
    <item>
      <title></title>
      <link>https://david.coffee/projects/quickshot/terms/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/projects/quickshot/terms/</guid><description>&lt;h1 id=&#34;terms-of-service-for-quickshot-ai-editor&#34;&gt;Terms of Service for Quickshot AI Editor&lt;/h1&gt;
&lt;p&gt;Last updated: 2025-05-02&lt;/p&gt;
&lt;h2 id=&#34;1-acceptance-of-terms&#34;&gt;1. Acceptance of Terms&lt;/h2&gt;
&lt;p&gt;By downloading, installing, or using Quickshot AI Editor (&amp;ldquo;the App&amp;rdquo;), you agree to be bound by these Terms of Service. If you do not agree to these terms, please do not use the App.&lt;/p&gt;
&lt;h2 id=&#34;2-description-of-service&#34;&gt;2. Description of Service&lt;/h2&gt;
&lt;p&gt;Quickshot AI Editor is an iOS application that uses artificial intelligence to edit photos based on natural language descriptions. The App requires an internet connection to process edits through third-party AI services.&lt;/p&gt;
&lt;h2 id=&#34;3-user-accounts&#34;&gt;3. User Accounts&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;You may be required to create an account using Sign in with Apple to use certain features&lt;/li&gt;
&lt;li&gt;You are responsible for maintaining the confidentiality of your account&lt;/li&gt;
&lt;li&gt;You agree to provide accurate information and to update it as necessary&lt;/li&gt;
&lt;li&gt;You are solely responsible for all activities that occur under your account&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;4-subscription-and-billing&#34;&gt;4. Subscription and Billing&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The App may offer subscription-based features&lt;/li&gt;
&lt;li&gt;Subscription fees will be clearly disclosed before purchase&lt;/li&gt;
&lt;li&gt;Subscriptions automatically renew unless canceled at least 24 hours before the end of the current period&lt;/li&gt;
&lt;li&gt;Subscription management and cancellation are handled through your Apple ID account settings&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;5-user-content&#34;&gt;5. User Content&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;You retain ownership of the photos you edit using the App&lt;/li&gt;
&lt;li&gt;By using the App, you grant us a limited license to process your photos for the purpose of providing the service&lt;/li&gt;
&lt;li&gt;You agree not to use the App to process or create:
&lt;ul&gt;
&lt;li&gt;Content that infringes on intellectual property rights&lt;/li&gt;
&lt;li&gt;Illegal, harmful, threatening, or abusive content&lt;/li&gt;
&lt;li&gt;Content that violates third-party terms of service&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;6-ai-processing&#34;&gt;6. AI Processing&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The App uses third-party AI services to process your photos and edit descriptions&lt;/li&gt;
&lt;li&gt;These services have their own terms of service and privacy policies&lt;/li&gt;
&lt;li&gt;We do not guarantee the accuracy, quality, or appropriateness of AI-generated edits&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;7-limitations-of-liability&#34;&gt;7. Limitations of Liability&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The App is provided &amp;ldquo;as is&amp;rdquo; without warranties of any kind&lt;/li&gt;
&lt;li&gt;We are not liable for any damages arising from your use of the App&lt;/li&gt;
&lt;li&gt;We are not responsible for the content of AI-generated edits&lt;/li&gt;
&lt;li&gt;We do not guarantee uninterrupted or error-free service&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;8-intellectual-property&#34;&gt;8. Intellectual Property&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The App, including its code, design, and features, is owned by us and protected by intellectual property laws&lt;/li&gt;
&lt;li&gt;You may not copy, modify, distribute, sell, or lease any part of the App&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;9-termination&#34;&gt;9. Termination&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;We reserve the right to suspend or terminate your access to the App at our discretion&lt;/li&gt;
&lt;li&gt;You may terminate your use of the App at any time by uninstalling it and canceling any subscriptions&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;10-changes-to-terms&#34;&gt;10. Changes to Terms&lt;/h2&gt;
&lt;p&gt;We may modify these Terms at any time. Continued use of the App after changes constitutes acceptance of the modified Terms.&lt;/p&gt;
&lt;h2 id=&#34;11-governing-law&#34;&gt;11. Governing Law&lt;/h2&gt;
&lt;p&gt;These Terms shall be governed by the laws of the jurisdiction in which the App developer is established, without regard to its conflict of law provisions.&lt;/p&gt;
&lt;h2 id=&#34;contact&#34;&gt;Contact&lt;/h2&gt;
&lt;p&gt;For questions about these Terms of Service, please &lt;a href=&#34;mailto:contact@davidmohl.com&#34;&gt;contact me&lt;/a&gt;&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<h1 id="terms-of-service-for-quickshot-ai-editor">Terms of Service for Quickshot AI Editor</h1>
<p>Last updated: 2025-05-02</p>
<h2 id="1-acceptance-of-terms">1. Acceptance of Terms</h2>
<p>By downloading, installing, or using Quickshot AI Editor (&ldquo;the App&rdquo;), you agree to be bound by these Terms of Service. If you do not agree to these terms, please do not use the App.</p>
<h2 id="2-description-of-service">2. Description of Service</h2>
<p>Quickshot AI Editor is an iOS application that uses artificial intelligence to edit photos based on natural language descriptions. The App requires an internet connection to process edits through third-party AI services.</p>
<h2 id="3-user-accounts">3. User Accounts</h2>
<ul>
<li>You may be required to create an account using Sign in with Apple to use certain features</li>
<li>You are responsible for maintaining the confidentiality of your account</li>
<li>You agree to provide accurate information and to update it as necessary</li>
<li>You are solely responsible for all activities that occur under your account</li>
</ul>
<h2 id="4-subscription-and-billing">4. Subscription and Billing</h2>
<ul>
<li>The App may offer subscription-based features</li>
<li>Subscription fees will be clearly disclosed before purchase</li>
<li>Subscriptions automatically renew unless canceled at least 24 hours before the end of the current period</li>
<li>Subscription management and cancellation are handled through your Apple ID account settings</li>
</ul>
<h2 id="5-user-content">5. User Content</h2>
<ul>
<li>You retain ownership of the photos you edit using the App</li>
<li>By using the App, you grant us a limited license to process your photos for the purpose of providing the service</li>
<li>You agree not to use the App to process or create:
<ul>
<li>Content that infringes on intellectual property rights</li>
<li>Illegal, harmful, threatening, or abusive content</li>
<li>Content that violates third-party terms of service</li>
</ul>
</li>
</ul>
<h2 id="6-ai-processing">6. AI Processing</h2>
<ul>
<li>The App uses third-party AI services to process your photos and edit descriptions</li>
<li>These services have their own terms of service and privacy policies</li>
<li>We do not guarantee the accuracy, quality, or appropriateness of AI-generated edits</li>
</ul>
<h2 id="7-limitations-of-liability">7. Limitations of Liability</h2>
<ul>
<li>The App is provided &ldquo;as is&rdquo; without warranties of any kind</li>
<li>We are not liable for any damages arising from your use of the App</li>
<li>We are not responsible for the content of AI-generated edits</li>
<li>We do not guarantee uninterrupted or error-free service</li>
</ul>
<h2 id="8-intellectual-property">8. Intellectual Property</h2>
<ul>
<li>The App, including its code, design, and features, is owned by us and protected by intellectual property laws</li>
<li>You may not copy, modify, distribute, sell, or lease any part of the App</li>
</ul>
<h2 id="9-termination">9. Termination</h2>
<ul>
<li>We reserve the right to suspend or terminate your access to the App at our discretion</li>
<li>You may terminate your use of the App at any time by uninstalling it and canceling any subscriptions</li>
</ul>
<h2 id="10-changes-to-terms">10. Changes to Terms</h2>
<p>We may modify these Terms at any time. Continued use of the App after changes constitutes acceptance of the modified Terms.</p>
<h2 id="11-governing-law">11. Governing Law</h2>
<p>These Terms shall be governed by the laws of the jurisdiction in which the App developer is established, without regard to its conflict of law provisions.</p>
<h2 id="contact">Contact</h2>
<p>For questions about these Terms of Service, please <a href="mailto:contact@davidmohl.com">contact me</a></p>
]]></content:encoded>
    </item>
    
    <item>
      <title></title>
      <link>https://david.coffee/projects/terms/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/projects/terms/</guid><description>&lt;h1 id=&#34;terms-of-use-for-my-applications&#34;&gt;Terms of Use for My Applications&lt;/h1&gt;
&lt;p&gt;Please review these terms carefully. They are intended to create a positive experience for both of us as you use my applications.&lt;/p&gt;
&lt;h2 id=&#34;overview&#34;&gt;Overview&lt;/h2&gt;
&lt;p&gt;By using any of my applications, you acknowledge and agree to these terms. I reserve the right to modify these terms at any time, and continued use constitutes acceptance of any changes. The most current version will always be available here.&lt;/p&gt;
&lt;h2 id=&#34;acceptable-usage&#34;&gt;Acceptable Usage&lt;/h2&gt;
&lt;p&gt;You are free to use my applications for their intended purposes without limitation. However, any use that could harm the applications or other users is strictly prohibited.&lt;/p&gt;
&lt;h2 id=&#34;copyright-and-ownership&#34;&gt;Copyright and Ownership&lt;/h2&gt;
&lt;p&gt;All content, code, and functionality within my applications remain my property and are protected by copyright laws. Unauthorized reproduction, modification, or distribution of any portion of my applications is not allowed without my explicit permission.&lt;/p&gt;
&lt;h2 id=&#34;app-marketplaces&#34;&gt;App Marketplaces&lt;/h2&gt;
&lt;p&gt;For applications obtained through official app stores, their respective terms of service will apply in addition to these terms.&lt;/p&gt;
&lt;h2 id=&#34;software-quality&#34;&gt;Software Quality&lt;/h2&gt;
&lt;p&gt;While I strive for excellence in all my applications, I cannot guarantee they will be entirely free of errors. I value your trust greatly, but recommend regular data backups as a precautionary measure.&lt;/p&gt;
&lt;h2 id=&#34;accessibility-commitment&#34;&gt;Accessibility Commitment&lt;/h2&gt;
&lt;p&gt;I am dedicated to ensuring my applications are accessible to all users, including those with disabilities. If you encounter accessibility barriers while using my applications, please contact me directly. I will work diligently to address these issues and continuously improve the accessibility of my products.&lt;/p&gt;
&lt;h2 id=&#34;limitation-of-liability&#34;&gt;Limitation of Liability&lt;/h2&gt;
&lt;p&gt;I cannot accept responsibility for any damages or issues arising from the use of my applications. By using these applications, you accept all risks associated with their use and any actions taken based on their content or functionality.&lt;/p&gt;
&lt;h2 id=&#34;user-indemnification&#34;&gt;User Indemnification&lt;/h2&gt;
&lt;p&gt;You agree to protect and indemnify me against any claims, damages, or expenses resulting from your use of my applications or violations of these terms.&lt;/p&gt;
&lt;h2 id=&#34;refund-policy&#34;&gt;Refund Policy&lt;/h2&gt;
&lt;p&gt;For purchases made through app marketplaces, their refund policies apply. However, I encourage you to contact me directly with any concerns before requesting a refund, as I may be able to resolve your issue.&lt;/p&gt;
&lt;h2 id=&#34;customer-support&#34;&gt;Customer Support&lt;/h2&gt;
&lt;p&gt;I strive to respond to all support inquiries within seven days, though response times may occasionally vary. Your patience and understanding are appreciated.&lt;/p&gt;
&lt;p&gt;Last Updated: 2025-03-12&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<h1 id="terms-of-use-for-my-applications">Terms of Use for My Applications</h1>
<p>Please review these terms carefully. They are intended to create a positive experience for both of us as you use my applications.</p>
<h2 id="overview">Overview</h2>
<p>By using any of my applications, you acknowledge and agree to these terms. I reserve the right to modify these terms at any time, and continued use constitutes acceptance of any changes. The most current version will always be available here.</p>
<h2 id="acceptable-usage">Acceptable Usage</h2>
<p>You are free to use my applications for their intended purposes without limitation. However, any use that could harm the applications or other users is strictly prohibited.</p>
<h2 id="copyright-and-ownership">Copyright and Ownership</h2>
<p>All content, code, and functionality within my applications remain my property and are protected by copyright laws. Unauthorized reproduction, modification, or distribution of any portion of my applications is not allowed without my explicit permission.</p>
<h2 id="app-marketplaces">App Marketplaces</h2>
<p>For applications obtained through official app stores, their respective terms of service will apply in addition to these terms.</p>
<h2 id="software-quality">Software Quality</h2>
<p>While I strive for excellence in all my applications, I cannot guarantee they will be entirely free of errors. I value your trust greatly, but recommend regular data backups as a precautionary measure.</p>
<h2 id="accessibility-commitment">Accessibility Commitment</h2>
<p>I am dedicated to ensuring my applications are accessible to all users, including those with disabilities. If you encounter accessibility barriers while using my applications, please contact me directly. I will work diligently to address these issues and continuously improve the accessibility of my products.</p>
<h2 id="limitation-of-liability">Limitation of Liability</h2>
<p>I cannot accept responsibility for any damages or issues arising from the use of my applications. By using these applications, you accept all risks associated with their use and any actions taken based on their content or functionality.</p>
<h2 id="user-indemnification">User Indemnification</h2>
<p>You agree to protect and indemnify me against any claims, damages, or expenses resulting from your use of my applications or violations of these terms.</p>
<h2 id="refund-policy">Refund Policy</h2>
<p>For purchases made through app marketplaces, their refund policies apply. However, I encourage you to contact me directly with any concerns before requesting a refund, as I may be able to resolve your issue.</p>
<h2 id="customer-support">Customer Support</h2>
<p>I strive to respond to all support inquiries within seven days, though response times may occasionally vary. Your patience and understanding are appreciated.</p>
<p>Last Updated: 2025-03-12</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>ConfigMesh</title>
      <link>https://david.coffee/projects/configmesh/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/projects/configmesh/</guid><description>&lt;p&gt;ConfigMesh runs as a native macOS app on your machine. It monitors your specified config files, encrypts them with your personal key, and syncs them across machines. Your encryption key never leaves your device, ensuring true end-to-end encryption.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>ConfigMesh runs as a native macOS app on your machine. It monitors your specified config files, encrypts them with your personal key, and syncs them across machines. Your encryption key never leaves your device, ensuring true end-to-end encryption.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Desks Companion for WeWork</title>
      <link>https://david.coffee/projects/desks/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/projects/desks/</guid><description>&lt;p&gt;Whether you&amp;rsquo;re a daily hot-desker or an occasional workspace user, Desks empowers you to manage your schedule and discover new workspaces at your convenience.&lt;/p&gt;
&lt;p&gt;This app is not affiliated with WeWork or its subsidiaries.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>Whether you&rsquo;re a daily hot-desker or an occasional workspace user, Desks empowers you to manage your schedule and discover new workspaces at your convenience.</p>
<p>This app is not affiliated with WeWork or its subsidiaries.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>eSIM DB</title>
      <link>https://david.coffee/projects/esimdb/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/projects/esimdb/</guid><description>&lt;p&gt;Browse and compare eSIM plans from multiple providers to find the best data plan for your destination. Whether you&amp;rsquo;re traveling to a single country or across multiple regions, eSIM DB makes it easy to find the right plan.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>Browse and compare eSIM plans from multiple providers to find the best data plan for your destination. Whether you&rsquo;re traveling to a single country or across multiple regions, eSIM DB makes it easy to find the right plan.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Fix My Japanese</title>
      <link>https://david.coffee/projects/fixmyjapanese/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/projects/fixmyjapanese/</guid><description></description>
      <content:encoded><![CDATA[]]></content:encoded>
    </item>
    
    <item>
      <title>MarkRight</title>
      <link>https://david.coffee/projects/markright/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/projects/markright/</guid><description></description>
      <content:encoded><![CDATA[]]></content:encoded>
    </item>
    
    <item>
      <title>Masked Email Manager</title>
      <link>https://david.coffee/projects/maskedemailmanager/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/projects/maskedemailmanager/</guid><description></description>
      <content:encoded><![CDATA[]]></content:encoded>
    </item>
    
    <item>
      <title>Maskwire</title>
      <link>https://david.coffee/projects/maskwire/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/projects/maskwire/</guid><description></description>
      <content:encoded><![CDATA[]]></content:encoded>
    </item>
    
    <item>
      <title>MCP Nest</title>
      <link>https://david.coffee/projects/mcpnest/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/projects/mcpnest/</guid><description>&lt;p&gt;Run your Model Context Protocol servers in the cloud without the hassle of local configuration. MCP Nest provides seamless access to your development and AI tools from any device, making it easy to use your favorite MCP tools with popular AI assistants.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>Run your Model Context Protocol servers in the cloud without the hassle of local configuration. MCP Nest provides seamless access to your development and AI tools from any device, making it easy to use your favorite MCP tools with popular AI assistants.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>microfn</title>
      <link>https://david.coffee/projects/microfn/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/projects/microfn/</guid><description>&lt;p&gt;Build and deploy serverless functions instantly without any setup. Share your functions with the community, remix existing ones, and integrate with AI assistants—all from a simple, powerful platform.&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>Build and deploy serverless functions instantly without any setup. Share your functions with the community, remix existing ones, and integrate with AI assistants—all from a simple, powerful platform.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>My Apps and projects</title>
      <link>https://david.coffee/projects/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/projects/</guid><description>&lt;p&gt;Welcome to my collection of apps &amp;amp; projects. Each one is crafted with care to solve specific problems.&lt;/p&gt;
&lt;p&gt;Curious about one? &lt;a href=&#34;mailto:contact@davidmohl.com&#34;&gt;Contact me!&lt;/a&gt;&lt;/p&gt;
</description>
      <content:encoded><![CDATA[<p>Welcome to my collection of apps &amp; projects. Each one is crafted with care to solve specific problems.</p>
<p>Curious about one? <a href="mailto:contact@davidmohl.com">Contact me!</a></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>The Enabler</title>
      <link>https://david.coffee/projects/enabler/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://david.coffee/projects/enabler/</guid><description></description>
      <content:encoded><![CDATA[]]></content:encoded>
    </item>
    
  </channel>
</rss>
