<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="/rss.xsl" media="all"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>Roastidio.us Tagged with elixir</title>
<link>https://roastidio.us/tag/2771</link>
<atom:link href="https://roastidio.us/tagged_with/elixir" rel="self" type="application/rss+xml"></atom:link>
<description>Roastidio.us Tagged with elixir</description>
<item>
<title>Privacy by Design no JurisOS: Criptografia AES-256 de Ponta a Ponta com Cloak.Ecto, SQLCipher e Tailscale VPN - DEV Community</title>
<link>https://dev.to/web-engenharia/privacy-by-design-no-jurisos-criptografia-aes-256-de-ponta-a-ponta-com-cloakecto-sqlcipher-e-108p</link>
<enclosure type="image/jpeg" length="0" url="https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fckqg2xavlz5q4el92hgq.png"></enclosure>
<guid isPermaLink="false">vWDqj2MZBtPUEKh7mtXPCAZ53QUAc4vqqKqeJA==</guid>
<pubDate>Sat, 29 Aug 2026 15:13:10 +0000</pubDate>
<description>Como blindar os dados de um escritório de advocacia em todas as pontas da arquitetura com Elixir, garantindo conformidade com LGPD, GDPR e CCPA.</description>
<content:encoded>&lt;p&gt;O setor jurídico lida diariamente com dados de altíssima sensibilidade: laudos médicos, segredos industriais, estratégias de litígio, dados bancários e informações de identificação pessoal (PII). Desenvolver um sistema como o &lt;strong&gt;JurisOS&lt;/strong&gt; — que opera sob o paradigma &lt;em&gt;Local-First&lt;/em&gt; nos desktops de secretárias e advogados — exige incorporar o conceito de &lt;strong&gt;Privacy by Design&lt;/strong&gt; desde a fundação da arquitetura.&lt;/p&gt;&lt;p&gt;Neste artigo, vamos detalhar como construímos uma malha de defesa em profundidade no ecossistema Elixir e BEAM, utilizando criptografia AES-256-GCM tanto em repouso quanto em trânsito, para garantir conformidade rigorosa com legislações de proteção de dados (LGPD, GDPR e CCPA).&lt;/p&gt;&lt;h2&gt;
  
  
  1. O Desafio da Conformidade (LGPD, GDPR e CCPA)
&lt;/h2&gt;&lt;p&gt;As leis modernas de proteção de dados exigem que os sistemas garantam a confidencialidade e a integridade das informações pessoais contra vazamentos, sejam eles acidentais (perda de um notebook) ou maliciosos (ataques de exfiltração).&lt;/p&gt;&lt;p&gt;Para um aplicativo desktop &lt;em&gt;Local-First&lt;/em&gt; sincronizado com a nuvem, a superfície de ataque é dupla:&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;O endpoint local:&lt;/strong&gt; O disco do computador físico do usuário no escritório.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;O transporte e o backend central:&lt;/strong&gt; A rede por onde os dados trafegam e o banco de dados principal (PostgreSQL) na nuvem.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;Para mitigar esses vetores, o JurisOS adota a &lt;strong&gt;Separação de Chaves (Key Isolation)&lt;/strong&gt; em múltiplas camadas.&lt;/p&gt;&lt;h2&gt;
  
  
  2. Separação de Chaves: SQLCipher e Cloak.Ecto
&lt;/h2&gt;&lt;p&gt;Não basta confiar apenas no acesso do usuário ao sistema operacional. Se um dispositivo for furtado, o banco de dados local não pode ser montado e lido por terceiros. Para resolver isso, separamos as responsabilidades de criptografia.&lt;/p&gt;&lt;h3&gt;
  
  
  Camada 1: Criptografia de Disco com SQLCipher (&lt;code&gt;SQLCIPHER_KEY&lt;/code&gt;)
&lt;/h3&gt;&lt;p&gt;Na ponta do cliente (o aplicativo desktop empacotado via Burrito), o armazenamento local não é um SQLite padrão, mas sim o &lt;strong&gt;SQLCipher&lt;/strong&gt;. Todo o arquivo &lt;code&gt;.db&lt;/code&gt; é cifrado transparentemente utilizando AES-256. &lt;br/&gt;
A &lt;code&gt;SQLCIPHER_KEY&lt;/code&gt; é gerada no momento do provisionamento da máquina e injetada no ambiente de execução. Isso protege contra ataques físicos: a extração do arquivo do disco resulta em bits indecifráveis sem a chave simétrica correta.&lt;/p&gt;&lt;h3&gt;
  
  
  Camada 2: Criptografia de Nível de Campo com Cloak.Ecto (&lt;code&gt;CLOAK_SECRET_KEY&lt;/code&gt;)
&lt;/h3&gt;&lt;p&gt;Enquanto o SQLCipher protege o contêiner físico, o &lt;strong&gt;Cloak.Ecto&lt;/strong&gt; protege o conteúdo lógico. Campos críticos que contêm PII (CPF, RG, históricos médicos) e a própria &lt;em&gt;Transaction Outbox&lt;/em&gt; de sincronização recebem uma camada adicional de AES-256-GCM a nível de aplicação.&lt;/p&gt;&lt;p&gt;Isso significa que mesmo um administrador de banco de dados (DBA) acessando a instância central do PostgreSQL na AWS, ou um atacante que consiga realizar um SQL Injection, verá apenas &lt;em&gt;ciphertexts&lt;/em&gt; binários, não os dados reais.&lt;br/&gt;
&lt;/p&gt;&lt;div&gt;
&lt;pre&gt;&lt;code&gt;defmodule JurisOS.Vault do
  use Cloak.Vault, otp_app: :juris_os

  @impl true
  def init(config) do
    config =
      Keyword.put(config, :aes_gcm,
        tag: &amp;quot;AES.GCM.V1&amp;quot;,
        key: decode_key(System.fetch_env!(&amp;quot;CLOAK_SECRET_KEY&amp;quot;))
      )

    {:ok, config}
  end

  defp decode_key(key) do
    Base.decode64!(key)
  end
end

defmodule JurisOS.Clientes.EncryptedString do
  use Cloak.Ecto.String, vault: JurisOS.Vault
end&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div&gt;
    

    

&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Na definição do Schema Ecto, mapeamos os campos sensíveis usando o nosso tipo customizado:&lt;br/&gt;
&lt;/p&gt;&lt;p&gt;A &lt;code&gt;CLOAK_SECRET_KEY&lt;/code&gt; fica estritamente na memória da BEAM. Os dados são cifrados antes de gerar o SQL de &lt;code&gt;INSERT&lt;/code&gt;/&lt;code&gt;UPDATE&lt;/code&gt; e decifrados logo após o &lt;code&gt;SELECT&lt;/code&gt;, mantendo o banco de dados completamente cego quanto ao conteúdo dos campos.&lt;/p&gt;&lt;h2&gt;
  
  
  3. Isolamento da Malha BEAM via Tailscale VPN
&lt;/h2&gt;&lt;p&gt;A comunicação entre os nós de desktop locais e o nó mestre na nuvem requer transporte seguro. Em vez de expor portas públicas na internet (&lt;code&gt;0.0.0.0&lt;/code&gt;) e confiar apenas na camada TLS das requisições web, o JurisOS eleva a segurança encapsulando toda a infraestrutura em uma rede &lt;em&gt;Zero Trust&lt;/em&gt;.&lt;/p&gt;&lt;p&gt;Utilizando a &lt;strong&gt;Tailscale VPN&lt;/strong&gt; (baseada em WireGuard), cada estação de trabalho e o servidor AWS recebem um IP de uma sub-rede privada (ex: &lt;code&gt;100.64.0.0/10&lt;/code&gt;).&lt;/p&gt;&lt;h3&gt;
  
  
  Os Benefícios dessa Arquitetura de Rede:
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Superfície de Ataque Reduzida a Zero:&lt;/strong&gt; O servidor na nuvem rejeita qualquer conexão que não venha de uma interface WireGuard. Não há portas expostas ao port scan público.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comunicação Nativa Erlang Distribution:&lt;/strong&gt; Podemos conectar os nós desktop ao cluster principal (&lt;code&gt;libcluster&lt;/code&gt;) trafegando mensagens do ecossistema OTP através do túnel da Tailscale de forma totalmente segura.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Criptografia Dupla em Trânsito:&lt;/strong&gt; Além da proteção nativa do protocolo (como WebSockets sobre WSS), os pacotes TCP/UDP são criptografados pelas chaves WireGuard ponto a ponto.&lt;/li&gt;
&lt;/ul&gt;&lt;h2&gt;
  
  
  Conclusão
&lt;/h2&gt;&lt;p&gt;Proteger dados jurídicos requer mais do que apenas conformidade no papel; exige garantias algorítmicas de segurança. &lt;/p&gt;&lt;p&gt;Ao combinar o &lt;strong&gt;SQLCipher&lt;/strong&gt; para proteção contra acesso físico indevido, o &lt;strong&gt;Cloak.Ecto&lt;/strong&gt; para cegar o banco de dados central perante dados sensíveis (PII) e a &lt;strong&gt;Tailscale VPN&lt;/strong&gt; para isolar completamente a comunicação de rede da internet pública, o JurisOS estabelece um estado da arte em &lt;em&gt;Privacy by Design&lt;/em&gt; no ecossistema Elixir. &lt;/p&gt;&lt;p&gt;Essa arquitetura garante que, independentemente da ponta — seja no laptop offline do advogado ou no servidor AWS na nuvem —, a conformidade com a LGPD, GDPR e CCPA esteja tecnicamente assegurada desde o primeiro byte de dados inserido.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Egghead is a note-taking app. - by Mark Wunsch</title>
<link>https://wunsch.substack.com/p/egghead-is-a-note-taking-app</link>
<enclosure type="image/jpeg" length="0" url="https://substackcdn.com/image/fetch/$s_!l3sJ!,w_1200,h_675,c_fill,f_jpg,q_auto:good,fl_progressive:steep,g_auto/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F705dd067-c88a-4aeb-a40a-47e31d78b0ff_1731x909.png"></enclosure>
<guid isPermaLink="false">tVFZFoawsSbQtUd1aHDR3XpgWRTcO-fYI3CqgQ==</guid>
<pubDate>Fri, 21 Aug 2026 01:08:22 +0000</pubDate>
<description>A founding essay</description>
<content:encoded>&lt;p&gt;&lt;a href=&quot;https://egghead.computer&quot;&gt;Egghead&lt;/a&gt;&lt;span&gt; is a note-taking app.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;There are some who have already decided that the next interesting thing in software is going to call itself “AI-native” or “agentic” or “the cognitive operating system of the future.” Egghead is not those things. Or rather, it is some of those things but only as a consequence of being a note-taking app in 2026.&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;a href=&quot;https://substackcdn.com/image/fetch/$s_!8h7B!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5130b946-cdec-49a2-932b-04dafa826ffd_1254x1254.png&quot;&gt;&lt;div&gt;&lt;picture&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!8h7B!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5130b946-cdec-49a2-932b-04dafa826ffd_1254x1254.png&quot; alt=&quot;Black-and-white line drawing of a bearded man wearing glasses, facing forward, with the top of his head cleanly sliced off. A red apple, cut horizontally, floats above the opening, aligned with the head and connected by its stem, with a small green leaf attached.&quot; title=&quot;Black-and-white line drawing of a bearded man wearing glasses, facing forward, with the top of his head cleanly sliced off. A red apple, cut horizontally, floats above the opening, aligned with the head and connected by its stem, with a small green leaf attached.&quot;/&gt;&lt;/picture&gt;&lt;/div&gt;&lt;/a&gt;&lt;figcaption&gt;Generated by ChatGPT Images 2.0&lt;/figcaption&gt;&lt;/figure&gt;&lt;/div&gt;&lt;h2&gt;The problem with notes&lt;/h2&gt;&lt;p&gt;We take notes because notes outlast thinking. Anyone who has kept a notebook, either physical or digital, for any length of time has had the experience of writing something down only to later be unable to find it. You know the note is there, somewhere, but you can’t, for the life of you, recall where it was or what it said.&lt;/p&gt;&lt;p&gt;&lt;span&gt;This central challenge of keeping notes gets worse with scale. A single notebook is searchable by hand. Ten notebooks are significantly more challenging. A folder of digital notes can be searched, but only if you remember the exact words you used, which you usually don’t because the whole reason you wrote the note in the first place was to externalize the thought so you could stop holding it. The note is supposed to do the remembering for you. Instead it just changed &lt;/span&gt;&lt;em&gt;where&lt;/em&gt;&lt;span&gt; the forgetting occurs.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;The work to create a &lt;/span&gt;&lt;em&gt;system&lt;/em&gt;&lt;span&gt; of note-taking has lasted nearly as long as the act of note-taking itself. The Renaissance period had &lt;/span&gt;&lt;em&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Commonplace_book&quot;&gt;commonplace books&lt;/a&gt;&lt;/em&gt;&lt;span&gt;. The Index Card was popularized by &lt;/span&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Carl_Linnaeus&quot;&gt;Carl Linnaeus&lt;/a&gt;&lt;span&gt; — a guy with strong opinions about structured information. In the 20th Century an obscure German sociologist named Niklas Luhmann took his note-taking seriously enough to build a card catalog of nearly ninety thousand interlinked notes, which he then used to write more than seventy books and nearly four hundred scholarly articles. The system he extensively used was &lt;/span&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Zettelkasten&quot;&gt;Zettelkasten&lt;/a&gt;&lt;span&gt;, which became a subject of his own research into systems theory and prefigured the &lt;/span&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/History_of_wikis&quot;&gt;Wiki&lt;/a&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;The 21st century has produced an entire category of software — Evernote, Obsidian, Notion, Roam, Bear, Apple Notes, Logseq — to name just a handful that popped into my head. Each one promising that &lt;/span&gt;&lt;em&gt;this time&lt;/em&gt;&lt;span&gt;, the notes will stay findable. I know I am not alone in having tried more than one of them, and sticking with it for a nontrivial amount of time before finding another shiny object promising untold cognitive reward.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;They all work, but they all suffer from the same limitations. No matter how good the search, or how clever the linking, or how disciplined you are about tagging, the system can only ever give back what you put in. The smartest thing in the room is still you.&lt;/p&gt;&lt;p&gt;This isn’t a failure of any specific tool, but a structural property of the whole category. The limit on what you can do with a notebook is your own memory of what’s in it. Which is, if you’re like me, not very good. Which is why I started writing things down in the first place.&lt;/p&gt;&lt;h3&gt;How to write good notes&lt;/h3&gt;&lt;p&gt;&lt;span&gt;There is a self-help sub-genre in your nearest global online bookstore dedicated to note-taking systems, and though I have a personal perspective of what makes a good system of notes, I find the more critical thing to answer is &lt;/span&gt;&lt;em&gt;where &lt;/em&gt;&lt;span&gt;the notes are and &lt;/span&gt;&lt;em&gt;how&lt;/em&gt;&lt;span&gt; they are made available to you.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;The best answer, as of this writing, is the same answer as it was fifty years ago: a series of files written in &lt;/span&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Plain_text&quot;&gt;plain text&lt;/a&gt;&lt;span&gt; on your storage disk. Many popular note-taking software applications tend to use proprietary formats in proprietary databases, accessed only through said proprietary application. Plain text files are the computer world’s universal interface, and are a core pillar of the &lt;/span&gt;&lt;a href=&quot;https://cscie2x.dce.harvard.edu/hw/ch01s06.html&quot;&gt;Unix philosophy&lt;/a&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;For Egghead, the notes are assumed to live as files in a directory formatted as either &lt;/span&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Markdown&quot;&gt;Markdown&lt;/a&gt;&lt;span&gt; or &lt;/span&gt;&lt;a href=&quot;https://orgmode.org/&quot;&gt;Org Mode&lt;/a&gt;&lt;span&gt; using &lt;/span&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Help:Wikitext#Wikilinks&quot;&gt;Wikilinks&lt;/a&gt;&lt;span&gt; to denote connections between them. When we circumscribe our written notes to proprietary formats, we reduce our ability to retrieve them. Which as stated earlier, is already challenging enough due to the limitations of our own memory. Plain text first.&lt;/span&gt;&lt;/p&gt;&lt;h2&gt;Why Artificial Intelligence&lt;/h2&gt;&lt;p&gt;&lt;span&gt;For most of recent history, the limitations of software notebooks were reflections of the limitations of physical notebooks: limitations of the human operator. Improvements in metadata, indexing, and search are still fundamentally bound by the user: you can only search for a word that has been explicitly written, and traverse relationships over metadata that the user has embedded. Software could not, in any meaningful sense, build a notebook that &lt;/span&gt;&lt;em&gt;read&lt;/em&gt;&lt;span&gt; what you wrote and &lt;/span&gt;&lt;em&gt;engaged&lt;/em&gt;&lt;span&gt; with it as a participant.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Large language models change this. Not because they are intelligent in any deep sense, but because they can read more text than I can hold in my head, and they can produce reasonable language about that text on demand. That is genuinely new.&lt;/p&gt;&lt;p&gt;So the obvious move is to point an AI assistant at your notes and let it go to town.&lt;/p&gt;&lt;h3&gt;The problems with AI in knowledge management&lt;/h3&gt;&lt;p&gt;&lt;span&gt;This is what the current generation of AI products is doing. Uploading files to ChatGPT, creating project knowledge in Claude, Notion AI on top of your Notion workspace, Cursor in your codebase… They all share the same shape: there is a knowledge base &lt;/span&gt;&lt;em&gt;over here&lt;/em&gt;&lt;span&gt; and a single AI assistant &lt;/span&gt;&lt;em&gt;over there&lt;/em&gt;&lt;span&gt; and the assistant can occasionally reach over to read from the knowledge base before answering your question.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;The most ambitious version of this shape, and the one that pushed me to build something different, is &lt;/span&gt;&lt;a href=&quot;https://openclaw.ai/&quot;&gt;OpenClaw&lt;/a&gt;&lt;span&gt;. An open-source personal AI assistant that you can run on your own machine, talk to from any messaging app, and connect to your files and tools. It’s genuinely good.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;But OpenClaw directly demonstrates the limitations of a single AI assistant: it &lt;/span&gt;&lt;em&gt;agrees with you&lt;/em&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.anthropic.com/research/towards-understanding-sycophancy-in-language-models&quot;&gt;It has to.&lt;/a&gt;&lt;span&gt; There is no structural pressure on it to do otherwise. When you ask it a question, it will give you an answer shaped like the question. You ask a leading question and it follows your lead. The &lt;/span&gt;&lt;a href=&quot;https://wunsch.substack.com/p/youre-absolutely-right?r=2taqw&quot;&gt;“You’re absolutely right!”&lt;/a&gt;&lt;span&gt; reflex is both well-trodden joke material as well as real architectural fact: a single agent, optimizing locally for “be helpful” will reliably converge toward whatever the user seems to want to hear. This is the shape of one-on-one assistance.&lt;/span&gt;&lt;/p&gt;&lt;h4&gt;The memory of AI assistants&lt;/h4&gt;&lt;p&gt;&lt;span&gt;What happens after a conversation with an AI assistant ends? Increasingly, they remember things from conversation to conversation (which wasn’t always the case). OpenClaw, for example, has a &lt;/span&gt;&lt;a href=&quot;https://docs.openclaw.ai/concepts/memory&quot;&gt;MEMORY.md&lt;/a&gt;&lt;span&gt; and workspace for persistence.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;The dominant pattern for memory in AI tooling is some flavor of retrieval-augmented generation (&lt;/span&gt;&lt;a href=&quot;https://blogs.nvidia.com/blog/what-is-retrieval-augmented-generation/&quot;&gt;RAG&lt;/a&gt;&lt;span&gt;). Notes and past conversations get chunked into passages, embedded into vectors, and stored. On each new prompt, the &lt;/span&gt;&lt;a href=&quot;https://www.langchain.com/blog/the-anatomy-of-an-agent-harness&quot;&gt;harness&lt;/a&gt;&lt;span&gt; pulls the chunks that look semantically nearest to the question and stuffs them into the model’s context window.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;In April, a more ambitious variant of the same impulse was articulated in &lt;/span&gt;&lt;a href=&quot;https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f&quot;&gt;Andrej Karpathy’s LLM Wiki&lt;/a&gt;&lt;span&gt; pattern, where instead of retrieving from raw sources at query time, an LLM agent incrementally compiles your notes into a structured wiki and then queries &lt;/span&gt;&lt;em&gt;that&lt;/em&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;Both of these patterns exhibit the same flaw. In RAG, retrieval is shaped by the prompt and the prompt is shaped by &lt;/span&gt;&lt;em&gt;you&lt;/em&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;RAG works on chunks, not documents — your essay’s argument structure is gone before the agent ever sees it. In the LLM Wiki version, the same bias gets baked in earlier: by the time the wiki entry exists, it’s been passed through the agent’s filter. The summary is cleaner than the source. That’s what makes it a really compelling “memory” product, but not a great note-taking product. Ambiguity in your notes gets edited toward coherence.&lt;/p&gt;&lt;p&gt;So both the shape of a single AI assistant and its memory formation produce a thinking partner who is very capable at creating a confident, well-organized version of what you’ve already decided you wanted to hear.&lt;/p&gt;&lt;p&gt;This is exactly the limitation of human users that leads many to abandon note-taking or continuously migrate from one note-taking system to the next. The notebook can only return what you remember to retrieve. The single AI assistant with RAG memory can only return what your prompt vocabulary aims at. So the assistant-plus-notes shape, even with modern persistence, has two failure modes that compound: a single agent cannot disagree with itself in any structurally reliable way, and a single agent’s memory pulls toward the framing you walked in with. Both failures point at the same fix.&lt;/p&gt;&lt;p&gt;You need more than one, and they need to share notes.&lt;/p&gt;&lt;h2&gt;Multi-Agent Systems&lt;/h2&gt;&lt;p&gt;The intuition is straightforward and very human: Teams beat individuals on most kinds of knowledge work, especially the kind where the failure mode is groupthink rather than skill gap. Peer review beats self-review. Code review beats no code review. We already know this about humans. We already build institutions around it. A single perspective on its own work has a structural blind spot and the cure is more perspectives.&lt;/p&gt;&lt;p&gt;&lt;span&gt;Apply that to the notebook problem. If a single agent has a tendency to agree with you, the fix is not to find a better single agent. The fix is to put a &lt;/span&gt;&lt;em&gt;second&lt;/em&gt;&lt;span&gt; agent in the room with a different disposition, and let them disagree with each other where you can watch. The peer review you’d want from a thoughtful colleague, manufactured at the structural level, in real time, on the body of work you actually care about.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;Many orchestration frameworks have arrived to multi-agent systems by a different route, but their shape does not lend itself to inherently better results. They default to &lt;/span&gt;&lt;em&gt;coordinator&lt;/em&gt;&lt;span&gt; and &lt;/span&gt;&lt;em&gt;specialists&lt;/em&gt;&lt;span&gt; — one agent dispatches tasks, others execute, and results aggregate at the top. This is a star topology — an org-chart fantasy of how teams work.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;The &lt;/span&gt;&lt;a href=&quot;https://subspace.kernel.org/vger.kernel.org.html&quot;&gt;Linux kernel mailing list&lt;/a&gt;&lt;span&gt; does not have a dispatcher delegating tasks for execution. An organization that adopts Slack does not have an executive function adjudicating every channel message. Even in environments where you would &lt;/span&gt;&lt;em&gt;expect&lt;/em&gt;&lt;span&gt; a rigid hierarchy — &lt;/span&gt;&lt;a href=&quot;https://davidmarquet.com/books/turn-the-ship-around-book/&quot;&gt;like a nuclear submarine&lt;/a&gt;&lt;span&gt; — the strict leader-follower model doesn’t always produce the best outcomes. In my own personal experience working in software teams, the teams that work are ones where the coordination is light and the communication is dense.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;The recent MAS research bears this out — graph topologies, where any agent can talk to any other agent, &lt;/span&gt;&lt;a href=&quot;https://arxiv.org/abs/2503.01935&quot;&gt;outperform star and tree shapes&lt;/a&gt;&lt;span&gt; on collaborative tasks.&lt;/span&gt;&lt;/p&gt;&lt;h3&gt;The capabilities of agents&lt;/h3&gt;&lt;p&gt;Once you commit to a team full of agents working in a shared knowledge store, a security and a quality problem emerge that single-agent systems can (and frequently do) ignore.&lt;/p&gt;&lt;p&gt;“How much should this agent be able to access and perform?” is both a security question and a quality question. It’s a security question because, as has been borne out, agents can and will leak internal secrets to external places or perform destructive actions. More agents means more potential for leakage. It’s a quality question because agents that can do everything tend to converge. Without distinct role definitions, the disagreement that made a multi-agent architecture useful in the first place dissolves into consensus. Capability scoping is the structural pressure that keeps the room from collapsing into agreement.&lt;/p&gt;&lt;p&gt;&lt;span&gt;This is the &lt;/span&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Principle_of_least_privilege&quot;&gt;principle of least privilege&lt;/a&gt;&lt;span&gt; applied to agentic systems. The same reason I, as adjunct faculty, do not have the keys to Columbia University’s Network Operations Center. Least privilege improves the quality of the output, because it preserves the structural diversity that makes a team perform better than an individual.&lt;/span&gt;&lt;/p&gt;&lt;h2&gt;What is Egghead?&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Egghead is a note-taking app.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The notes are written as plain-text files in a folder you own.&lt;/p&gt;&lt;p&gt;It uses a loosely-coordinated group of AI agents to continuously read from and contribute to the total knowledge base, producing the most diverse set of inferences about that knowledge by allowing the user to grant each agent a distinct set of capabilities.&lt;/p&gt;&lt;p&gt;Every agent is itself defined as a note, as are the transcripts of their conversations and individual deliberations, giving each note provenance for its creation.&lt;/p&gt;&lt;p&gt;The notes are written as plain text in the filesystem, and the app exposes this and communication with its agents through as many surfaces as possible — the terminal, the web, MCP, and IRC — so that your knowledge is not locked behind any one of them.&lt;/p&gt;&lt;h2&gt;The goal of taking notes in Egghead&lt;/h2&gt;&lt;p&gt;&lt;span&gt;It is worth restating, we take notes because notes outlast thinking. Simply stated, the goal is to extend our thoughts beyond the storage and time limitations of our own &lt;/span&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Wetware_(brain)&quot;&gt;wetware&lt;/a&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;The goal is &lt;/span&gt;&lt;strong&gt;not&lt;/strong&gt;&lt;span&gt; productivity. The goal is &lt;/span&gt;&lt;strong&gt;not&lt;/strong&gt;&lt;span&gt; to get more done. The goal is not to have your meetings summarized, or your emails triaged, or your tasks auto-prioritized. Those are nice. They are not the point. The point — the actual, unfashionable, embarrassing-to-say-in-a-funding-pitch point — is to &lt;/span&gt;&lt;em&gt;get smarter&lt;/em&gt;&lt;span&gt;. To retain more of what you read. To do more with what you retain. To engage with your own past thinking.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;Every “AI assistant” on the market is a productivity tool, by which I mean a tool whose purpose is to enable you to do less of a thing and produce more output. A note-taking app built on this premise would exist to &lt;/span&gt;&lt;em&gt;reduce&lt;/em&gt;&lt;span&gt; the time you spend with your notes. I want the opposite. I want a tool that makes you spend &lt;/span&gt;&lt;em&gt;more&lt;/em&gt;&lt;span&gt; time with your notes. That makes the doing of it more rewarding.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;Knowledge itself is the outcome. The goal of the practice is – to use a word that has fallen out of fashion in software but used to be considered the most valuable thing a thinking person could accumulate – &lt;/span&gt;&lt;strong&gt;Wisdom&lt;/strong&gt;&lt;span&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Ipsa cognitio fructus&lt;/em&gt;&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Using Elixir head version with Mise</title>
<link>https://katafrakt.me/2026/01/03/elixir-head-with-mise/</link>
<guid isPermaLink="false">_n4BtwYDV3z0icZ1XH9AMdZCbnlwMUvJMOl8ww==</guid>
<pubDate>Sun, 16 Aug 2026 10:18:59 +0000</pubDate>
<description>In 2025 I followed the direction many people headed in and switched my version manager to mise-en-place. It has been going really well for me, but recently I came across a problem which took me a bit to figure out (and few very frustrating conversations with LLM, which did not lead to anything). I wanted to test some code with unreleased version 1.20 of Elixir, straight from git.</description>
<content:encoded>&lt;p&gt;In 2025 I followed the direction many people headed in and switched my version manager to &lt;a href=&quot;https://mise.jdx.dev&quot;&gt;mise-en-place&lt;/a&gt;. It has been going really well for me, but recently I came across a problem which took me a bit to figure out (and few &lt;em&gt;very&lt;/em&gt; frustrating conversations with LLM, which did not lead to anything). I wanted to test some code with unreleased version 1.20 of Elixir, straight from git.&lt;/p&gt;&lt;p&gt;How to do it? It’s really simple in retrospect. Here are the steps:&lt;/p&gt;&lt;h3&gt;1. Build Elixir&lt;/h3&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;$ git clone https://github.com/elixir-lang/elixir.git 
$ cd elixir
$ make&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Verify it’s built correctly:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;$ bin/elixir --version
&amp;gt; Elixir 1.20.0-dev (88cbabf) (compiled with Erlang/OTP 28)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3&gt;2. Link it in mise&lt;/h3&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;mise link elixir@head /path/to/elixir&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3&gt;3. Verify this works&lt;/h3&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;$ mise x elixir@head -- elixir --version
&amp;gt; Elixir 1.20.0-dev (88cbabf) (compiled with Erlang/OTP 28)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;And it’s done. Whenever you want to run something with Elixir head, you can just &lt;code class=&quot;highlighter-rouge&quot;&gt;mise use elixir@head&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Of course, this should work in a similar way for everything that is handles by mise version control.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Ecto, on_replace and deferred checks</title>
<link>https://katafrakt.me/2025/07/03/ecto-on-replace-deferred-check/</link>
<guid isPermaLink="false">kyx7kN8uLhXXKeuiC9Uumqf1-pgw4MX_OcPjjw==</guid>
<pubDate>Sun, 16 Aug 2026 10:18:59 +0000</pubDate>
<description>Today I learned a valuable lesson about how a seemingly simple task can have very rough edge cases, which take hours to solve. It involved Ecto, its associations and on_replace option, and uniqueness checks in the database. Here’s the story.</description>
<content:encoded>&lt;p&gt;Today I learned a valuable lesson about how a seemingly simple task can have very rough edge cases, which take hours to solve. It involved Ecto, its associations and &lt;code class=&quot;highlighter-rouge&quot;&gt;on_replace&lt;/code&gt; option, and uniqueness checks in the database. Here’s the story.&lt;/p&gt;&lt;h2&gt;The problem&lt;/h2&gt;&lt;p&gt;Let’s say you are modelling some kind of processes. These processes have steps and the steps have to be executed in a precise order. This is how a database structure for it would look like:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;defchangedocreatetable(:processes)doadd:name,:string,null:falseendcreatetable(:steps)doadd:process_id,references(:processes)add:name,:string,null:falseadd:order,:integer,null:falseendend&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;It is fairly straightforward: the order of steps inside of a process is controlled by an &lt;code class=&quot;highlighter-rouge&quot;&gt;order&lt;/code&gt; integer column. Since it is important to always have the order of steps precise, we would like to additionally ensure it by a uniqie index:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;createunique_index(:steps,[:process_id,:order])&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;We also know that the steps are only edited via a parent process. There is a form in the application, where you can edit the process fields and add, change, delete steps for it. The payload sent to a server always includes all the steps. Armed with that knowledge we create Ecto schemas like this:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;defmoduleCore.ProcessdouseEcto.SchemaimportEcto.Changesetschema&amp;quot;processes&amp;quot;dofield:name,:stringhas_many:steps,Core.Step,on_replace::delete,preload_order:[asc::order]enddefchangeset(process,params)doprocess|&amp;gt;cast(params,[:name])|&amp;gt;cast_assoc(:steps)endenddefmoduleCore.StepdouseEcto.SchemaimportEcto.Changesetschema&amp;quot;steps&amp;quot;dofield:name,:stringfield:order,:integerbelongs_to:process,Core.Processenddefchangeset(step,params)dostep|&amp;gt;cast(params,[:name,:order])endend&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;I did that, quite happily, tested a bit with adding, changing and removing some steps. Finally I merged the pull request.&lt;/p&gt;&lt;p&gt;It was only a few hours later when a colleague slacked me that something goes wrong. What he did was reordering the steps, or rather trying to do it. From this structure:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;step 1, order: 1
step 2, order: 2&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;he wanted to go to&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;step 1, order: 2
step 2, order: 1&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;I quickly drafted code that replicated the issue:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;proc=%Process{}|&amp;gt;Process.changeset(%{name:&amp;quot;Test&amp;quot;,steps:[%{order:1,name:&amp;quot;First&amp;quot;},%{order:2,name:&amp;quot;Second&amp;quot;}]})|&amp;gt;Repo.insert!()proc=Repo.preload(proc,[:steps])[s1,s2]=proc.stepss1=%{id:s1.id,name:s1.name,order:2}s2=%{id:s2.id,name:s2.name,order:1}proc|&amp;gt;Process.changeset(%{steps:[s1,s2]})|&amp;gt;Repo.update!()&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Attempt to run it resulted in an error:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;** (Ecto.ConstraintError) constraint error when attempting to update struct:

    * &amp;quot;steps_process_id_order_index&amp;quot; (unique_constraint)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;It took me a while to realize what’s going on: Ecto tried to update the existing steps, one by one. So the first operation was:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Update “order” of step 1 to 2&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;This left us with the following situation:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;step 1, order: 2
step 2, order: 2&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This, quite obviously in retrospect, triggered the unique index. You cannot have 2 steps for one process with the same order! It did not matter that right after you were going to update step 2’s order to 1. The index operates here and now, you cannot do that.&lt;/p&gt;&lt;h2&gt;The solution&lt;/h2&gt;&lt;p&gt;After discovering I was pretty close to writing my own hacky solution to update the steps, including inefficient approach with first deleting all and then creating all anew. Fortunately, in time, I vaguely remembered that there is something called &lt;a href=&quot;https://www.postgresql.org/docs/current/sql-set-constraints.html&quot;&gt;deferrable constraints&lt;/a&gt; in PostgreSQL.&lt;/p&gt;&lt;p&gt;Deferrable constraint waits until the end of the transaction with checking if its condition is met. This was exactly what I was looking for! But an index cannot be deferrable in PostgreSQL. Luckily, another very similar construct - a uniqueness check - can.&lt;/p&gt;&lt;p&gt;In the migration above, I had to replace the index creation with this:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;execute&amp;quot;&amp;quot;&amp;quot;
ALTER TABLE steps ADD CONSTRAINT unique_step UNIQUE (process_id, &amp;quot;order&amp;quot;) DEFERRABLE INITIALLY DEFERRED
&amp;quot;&amp;quot;&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;With this statement we create a constraint on the &lt;code class=&quot;highlighter-rouge&quot;&gt;steps&lt;/code&gt; table which checks uniqueness of two columns and not only is deferrable - it is deferred to be checked on transaction commit by default.&lt;/p&gt;&lt;p&gt;This is really all I had to do. The Ecto’s association replacing mechanism started working perfectly.&lt;/p&gt;&lt;p&gt;This proved once again a truth I knew: the database is not a dumb storage you can just gloss over. It always pays off to learn it, its capabilities and intricacies. Because then you end up with a simple change instead of rewriting part of Ecto, but poorly.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>the phoenix template has too much stuff</title>
<link>https://foxgirl.engineering/blog/phoenix-template-too-big</link>
<guid isPermaLink="false">lKrHkMye7KaUDu7zAaBVvaMdOIdf3KlwuZ7iCg==</guid>
<pubDate>Mon, 10 Aug 2026 19:36:52 +0000</pubDate>
<description>Seriously, who needs all of this?</description>
<content:encoded>&lt;p&gt;So, I&amp;#39;m starting a new project. I want to get this blog setup to send and receive &lt;a href=&quot;https://www.w3.org/TR/webmention/&quot;&gt;Webmentions&lt;/a&gt;, so that I can do some fun social stuff here. Comments, linking to other people talking about what I write, that sorta thing. I started out by trying to implement it into the blogging engine itself, but I felt like I was running up against the design of &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt; (the framework powering this place) trying to squeeze a public API and a message queue into what&amp;#39;s basically just an on-demand static site generator. So, I&amp;#39;m gonna try write an external server instead.
&lt;/p&gt;&lt;p&gt;My language of choice for building web servers lately has been &lt;a href=&quot;https://elixir-lang.org/&quot;&gt;Elixir&lt;/a&gt;. It&amp;#39;s a decently nice language with a crazy powerful runtime - &lt;a href=&quot;https://www.erlang.org/&quot;&gt;Erlang and the OTP&lt;/a&gt; do not fuck around. Usually I prefer to write servers from scratch, since it keeps the initial architecture nice and small, while giving me room to expand when I need some other particular feature. This time around though, I know I&amp;#39;m going to want a few things right off the bat:
&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Some sort of worker pool, so processing Webmentions can be done asynchronously without blocking the rest of the server
&lt;/li&gt;&lt;li&gt;A database to hold records of all the Webmentions that come my way
&lt;/li&gt;&lt;li&gt;Some HTML rendering so I can have status pages for senders to see if their request was processed, and also an admin panel so I can easily review incoming Webmentions
&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;At this point, I think we&amp;#39;re starting to get complex enough that I&amp;#39;d benefit from bringing in &lt;a href=&quot;https://www.phoenixframework.org/&quot;&gt;Phoenix&lt;/a&gt;, the de facto standard web app framework for Elixir. It&amp;#39;s got most of this loaded in and ready to go (&lt;a href=&quot;https://www.phoenixframework.org/blog/phoenix-liveview-1.0-released&quot;&gt;LiveView&lt;/a&gt; in particular sounds great for my admin panel stuff), so I figure it&amp;#39;s probably smart to start off with it.
&lt;/p&gt;&lt;p&gt;So, I make a new Phoenix project. Easy enough - &lt;a href=&quot;https://hexdocs.pm/phoenix/installation.html&quot;&gt;docs&lt;/a&gt; say:
&lt;/p&gt;&lt;pre&gt;&lt;code&gt;mix archive.install phx_newmix phx.new project_name&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So I run it, and...
&lt;/p&gt;&lt;pre&gt;&lt;samp&gt;* creating project_name/lib/project_name/application.ex
* creating project_name/lib/project_name.ex
* creating project_name/lib/project_name_web/controllers/error_json.ex
* creating project_name/lib/project_name_web/endpoint.ex
* creating project_name/lib/project_name_web/router.ex
* creating project_name/lib/project_name_web/telemetry.ex
* creating project_name/lib/project_name_web.ex
* creating project_name/mix.exs
* creating project_name/README.md
* creating project_name/.formatter.exs
* creating project_name/.gitignore
* creating project_name/test/support/conn_case.ex
* creating project_name/test/test_helper.exs
* creating project_name/test/project_name_web/controllers/error_json_test.exs
* creating project_name/lib/project_name/repo.ex
* creating project_name/priv/repo/migrations/.formatter.exs
* creating project_name/priv/repo/seeds.exs
* creating project_name/test/support/data_case.ex
* creating project_name/lib/project_name_web/controllers/error_html.ex
* creating project_name/test/project_name_web/controllers/error_html_test.exs
* creating project_name/lib/project_name_web/components/core_components.ex
* creating project_name/lib/project_name_web/controllers/page_controller.ex
* creating project_name/lib/project_name_web/controllers/page_html.ex
* creating project_name/lib/project_name_web/controllers/page_html/home.html.heex
* creating project_name/test/project_name_web/controllers/page_controller_test.exs
* creating project_name/lib/project_name_web/components/layouts/root.html.heex
* creating project_name/lib/project_name_web/components/layouts/app.html.heex
* creating project_name/lib/project_name_web/components/layouts.ex
* creating project_name/priv/static/images/logo.svg
* creating project_name/lib/project_name/mailer.ex
* creating project_name/lib/project_name_web/gettext.ex
* creating project_name/priv/gettext/en/LC_MESSAGES/errors.po
* creating project_name/priv/gettext/errors.pot
* creating project_name/priv/static/robots.txt
* creating project_name/priv/static/favicon.ico
* creating project_name/assets/js/app.js
* creating project_name/assets/vendor/topbar.js
* creating project_name/assets/css/app.css
* creating project_name/assets/tailwind.config.js
&lt;/samp&gt;&lt;/pre&gt;&lt;p&gt;...that&amp;#39;s a lot of stuff. What &lt;em&gt;is&lt;/em&gt; all this?
&lt;/p&gt;&lt;pre&gt;&lt;code&gt;# Configures the mailer## By default it uses the &amp;quot;Local&amp;quot; adapter which stores the emails# locally. You can see the emails in your browser, at &amp;quot;/dev/mailbox&amp;quot;.## For production it&amp;#39;s recommended to configure a different adapter# at the `config/runtime.exs`.config :project_name, ProjectName.Mailer, adapter: Swoosh.Adapters.Local&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;What
&lt;/p&gt;&lt;pre&gt;&lt;code&gt;# Configure esbuild (the version is required)config :esbuild,  version: &amp;quot;0.17.11&amp;quot;,  project_name: [    args:      ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),    cd: Path.expand(&amp;quot;../assets&amp;quot;, __DIR__),    env: %{&amp;quot;NODE_PATH&amp;quot; =&amp;gt; Path.expand(&amp;quot;../deps&amp;quot;, __DIR__)}  ]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;No, stop
&lt;/p&gt;&lt;pre&gt;&lt;code&gt;# Configure tailwind (the version is required)config :tailwind,  version: &amp;quot;3.4.3&amp;quot;,  project_name: [    args: ~w(      --config=tailwind.config.js      --input=css/app.css      --output=../priv/static/assets/app.css    ),    cd: Path.expand(&amp;quot;../assets&amp;quot;, __DIR__)  ]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;PLEASE
&lt;/p&gt;&lt;pre&gt;&lt;code&gt;# Specifies your project dependencies.## Type `mix help deps` for examples and options.defp deps do  [    {:phoenix, &amp;quot;~&amp;gt; 1.7.21&amp;quot;},    {:phoenix_ecto, &amp;quot;~&amp;gt; 4.5&amp;quot;},    {:ecto_sql, &amp;quot;~&amp;gt; 3.10&amp;quot;},    {:postgrex, &amp;quot;&amp;gt;= 0.0.0&amp;quot;},    {:phoenix_html, &amp;quot;~&amp;gt; 4.1&amp;quot;},    {:phoenix_live_reload, &amp;quot;~&amp;gt; 1.2&amp;quot;, only: :dev},    {:phoenix_live_view, &amp;quot;~&amp;gt; 1.0&amp;quot;},    {:floki, &amp;quot;&amp;gt;= 0.30.0&amp;quot;, only: :test},    {:phoenix_live_dashboard, &amp;quot;~&amp;gt; 0.8.3&amp;quot;},    {:esbuild, &amp;quot;~&amp;gt; 0.8&amp;quot;, runtime: Mix.env() == :dev},    {:tailwind, &amp;quot;~&amp;gt; 0.2.0&amp;quot;, runtime: Mix.env() == :dev},    {:heroicons,     github: &amp;quot;tailwindlabs/heroicons&amp;quot;,     tag: &amp;quot;v2.1.1&amp;quot;,     sparse: &amp;quot;optimized&amp;quot;,     app: false,     compile: false,     depth: 1},    {:swoosh, &amp;quot;~&amp;gt; 1.5&amp;quot;},    {:finch, &amp;quot;~&amp;gt; 0.13&amp;quot;},    {:telemetry_metrics, &amp;quot;~&amp;gt; 1.0&amp;quot;},    {:telemetry_poller, &amp;quot;~&amp;gt; 1.0&amp;quot;},    {:gettext, &amp;quot;~&amp;gt; 0.26&amp;quot;},    {:jason, &amp;quot;~&amp;gt; 1.2&amp;quot;},    {:dns_cluster, &amp;quot;~&amp;gt; 0.1.1&amp;quot;},    {:bandit, &amp;quot;~&amp;gt; 1.5&amp;quot;}  ]end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;OH GOD WHY ARE THERE SO MANY DEPENDENCIES AAAAAAA&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Seriously, who needs all this??? Why does the &lt;em&gt;starter template&lt;/em&gt; come preloaded with a fucking library for &lt;em&gt;sending emails&lt;/em&gt;? This isn&amp;#39;t basic starter stuff! I don&amp;#39;t need bloody &lt;em&gt;Tailwind&lt;/em&gt; to write HTML templates! There&amp;#39;s just so much stuff in this starter template - I probably need like, half of it? I thought I would just get an Elixir project with the Phoenix dependencies ready to go, and maybe a simple skeleton with a one-page &amp;quot;Welcome to Phoenix!&amp;quot; included, not the entire god damn kitchen sink? Why? WHY is there so much here??? Aaaaaah!!!
&lt;/p&gt;&lt;p&gt;&lt;em&gt;deep breath in&lt;/em&gt;&lt;/p&gt;&lt;pre&gt;&lt;code&gt;rm -rf project_namemix new project_name&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;em&gt;exhale&lt;/em&gt;. There we go. That&amp;#39;s better. Time to see how possible it is to set this up from scratch.
&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.petecorey.com/blog/2019/05/20/minimum-viable-phoenix/&quot;&gt;Not that hard&lt;/a&gt;? Nice. My sanity survives another day.
&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Anti-Antipatterns in Elixir Library Guidelines</title>
<link>https://rocket-science.ru/hacking/2026/08/09/anti-antipatterns-in-elixir-libraries</link>
<enclosure type="image/jpeg" length="0" url="https://rocket-science.ru/img/logo/logo-orig.png"></enclosure>
<guid isPermaLink="false">i7qrgCmxRFPiZnUwq-AeJr-ETFFN8xsjtFzfQg==</guid>
<pubDate>Sun, 09 Aug 2026 20:09:44 +0000</pubDate>
<description>Deconstructing the dogmatic anti-patterns section of the official Elixir library guidelines and demonstrating when every single one of them is actually needed.</description>
<content:encoded>&lt;p&gt;Only a lazy person never blogged about library should not use application configuration. Even the official propaganda says so in the &lt;a href=&quot;https://elixir.hexdocs.pm/1.12.3/library-guidelines.html#avoid-application-configuration&quot;&gt;Elixir Library Guidelines&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;As all the sharp rules, this one is rotten. There are usecases when the library configuration must prevail over local parameters.&lt;/p&gt;&lt;p&gt;In fact, the entire “Anti-patterns” section of the official library guidelines reads like a list of commandments handed down from Mount Sinai, written for developers who cannot be trusted with sharp tools. But software engineering is not about blindly following rigid rules; it is about choosing the right trade-offs.&lt;/p&gt;&lt;p&gt;Let’s walk through all nine anti-patterns declared in the official guidelines and show why—and when—each so-called “anti-pattern” is not just permissible, but absolutely necessary.&lt;/p&gt;&lt;hr/&gt;&lt;h2&gt;1. Avoid Application Configuration&lt;/h2&gt;&lt;p&gt;The guidelines state that libraries must never rely on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Application.get_env/2&lt;/code&gt; because global state makes it impossible for two dependencies to use the library in different ways. Instead, we are told to pass explicit keyword options down every function call.&lt;/p&gt;&lt;p&gt;Consider an application dealing with markdown here and there. Its dependencies parse markdown too. Core parses markdown, web parses markdown, several helpers under umbrella do parse markdown.&lt;/p&gt;&lt;p&gt;How do I add the new plugin everywhere? Easy, if there is an application configuration. And …ehrm… there is a way to patch dependencies.&lt;/p&gt;&lt;p&gt;If you enforce passing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;opts&lt;/code&gt; explicitly through forty layers of call stacks across fifteen sub-applications in an umbrella project, you guarantee that every intermediate module becomes a glorified passthrough for configuration keys it doesn’t care about. Application configuration allows the system operator to define system-wide defaults once—in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config/config.exs&lt;/code&gt;—while still allowing individual calls to override them when necessary. Global defaults are a feature, not a bug, when your application needs uniform behavior by default.&lt;/p&gt;&lt;hr/&gt;&lt;h2&gt;2. Avoid Compile-Time Application Configuration&lt;/h2&gt;&lt;p&gt;We are warned against reading the application environment in module attributes, such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@http_client Application.fetch_env!(...)&lt;/code&gt;, because it burns values into the compiled binary at compile time rather than reading them at runtime.&lt;/p&gt;&lt;p&gt;This advice conveniently ignores two crucial requirements: &lt;strong&gt;zero-cost abstractions&lt;/strong&gt; and &lt;strong&gt;compile-time code generation&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;When building high-throughput data pipelines, parsers, or logging frameworks, invoking &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Application.get_env/3&lt;/code&gt; inside a hot loop running millions of times per second introduces unnecessary dictionary lookups. Statically baking configured modules or constants into module attributes yields zero-overhead dispatch.&lt;/p&gt;&lt;p&gt;Furthermore, if your library uses macros to generate pattern-matching clauses or AST structures based on configuration (e.g., compiling custom sigils or state machine transitions dynamically), runtime lookup is literally impossible. The AST must be constructed at compile time. Compile-time configuration is the bridge between user configuration and macro expansion.&lt;/p&gt;&lt;hr/&gt;&lt;h2&gt;3. Avoid Using Exceptions for Control-Flow&lt;/h2&gt;&lt;p&gt;“Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:ok, result}&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:error, reason}&lt;/code&gt; tuples everywhere,” says the guide. “Never use exceptions or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;raise&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;throw&lt;/code&gt; for control flow.”&lt;/p&gt;&lt;p&gt;This sounds clean in trivial examples, but consider a deeply nested AST traversal or a recursive tree parser fifteen levels deep. If an invalid node is encountered on level 14, propagating &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:error, reason}&lt;/code&gt; back up through 14 stack frames requires wrapping every single recursive step in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;with&lt;/code&gt; block or manual tuple matching. The core algorithm becomes buried under an avalanche of error-plumbing boilerplate.&lt;/p&gt;&lt;p&gt;In complex recursive operations, using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;throw/1&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;catch&lt;/code&gt; (or raising a dedicated structural exception rescued at the top-level boundary) acts as a clean, non-local exit. It unwinds the stack immediately back to the public API boundary, leaving the internal recursive code concise, readable, and fast. The caller still receives a clean &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:error, reason}&lt;/code&gt; tuple at the boundary, but the internal code doesn’t suffer from tuple-wrapping tax.&lt;/p&gt;&lt;p&gt;Even &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;for&lt;/code&gt; comprehension might take advantage from throwing in the middle if something went south.&lt;/p&gt;&lt;hr/&gt;&lt;h2&gt;4. Avoid Working with Invalid Data&lt;/h2&gt;&lt;p&gt;The guideline advises validating all data immediately at the entry boundary using pattern matching and guards, refusing to operate on anything that doesn’t strictly match expected types.&lt;/p&gt;&lt;p&gt;This principle breaks down when dealing with &lt;strong&gt;streaming data ingestion&lt;/strong&gt;, &lt;strong&gt;resilient ETL pipelines&lt;/strong&gt;, and &lt;strong&gt;progressive parsing of semi-structured data&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Suppose you are building a log processor or IoT event consumer that ingests gigabytes of heterogeneous payloads. If you strictly validate the entire schema at the entry boundary, a single malformed key or unexpected field type forces you to drop the whole batch or crash the pipeline.&lt;/p&gt;&lt;p&gt;Instead, accepting loose structures at the boundary and deferring validation to downstream processing stages allows your system to handle partial data, quarantine bad sub-fields, and log warnings without discarding valid payload portions. Late validation and resilient boundary accepting are fundamental to building fault-tolerant data pipelines.&lt;/p&gt;&lt;hr/&gt;&lt;h2&gt;5. Avoid Defining Modules Outside Your Namespace&lt;/h2&gt;&lt;p&gt;The rules demand that every module in a library be prefixed with the library’s top-level module name (e.g., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MyLib.Foo&lt;/code&gt;), warning that defining top-level modules or extending another library’s namespace causes collisions on the Erlang VM.&lt;/p&gt;&lt;p&gt;Yet, this rule completely ignores &lt;strong&gt;drop-in polyfills&lt;/strong&gt;, &lt;strong&gt;standard protocol implementations&lt;/strong&gt;, and &lt;strong&gt;seamless stdlib integration&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;If you are writing a drop-in replacement library or a compatibility layer (for instance, a library providing backwards compatibility for a deprecated OTP module or patching telemetry integration like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Telemetria&lt;/code&gt;), placing your modules in the expected target namespace is the entire point. It allows existing applications to use your library without modifying hundreds of call sites across an entire codebase.&lt;/p&gt;&lt;p&gt;Similarly, when implementing protocol consolidations or global dispatchers that third-party plugins hook into, adhering strictly to arbitrary prefixing can make macro-based registration unwieldy. When done intentionally, target-namespaced modules are a legitimate pattern for drop-in interoperability.&lt;/p&gt;&lt;hr/&gt;&lt;h2&gt;6. Avoid &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;use&lt;/code&gt; When an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import&lt;/code&gt; Is Enough&lt;/h2&gt;&lt;p&gt;“Do not provide &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;use MyLib&lt;/code&gt; if all it does is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import MyLib&lt;/code&gt;,” reads the guide. “Prefer &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;alias&lt;/code&gt;, then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import&lt;/code&gt;, and use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;use&lt;/code&gt; only as a last resort.”&lt;/p&gt;&lt;p&gt;This is classic short-term thinking that ruins library API evolution.&lt;/p&gt;&lt;p&gt;Suppose version 1.0 of your library only needs to import helper functions into the target module. You instruct users to put &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import MyLib&lt;/code&gt; in their code. Six months later, in version 2.0, your library needs to register module attributes, inject a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@before_compile&lt;/code&gt; hook, or set up telemetry callbacks.&lt;/p&gt;&lt;p&gt;Because you chose &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import MyLib&lt;/code&gt; to satisfy a dogmatic guideline, every single user of your library must now go through their codebase and replace &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import MyLib&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;use MyLib&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;By offering &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;use MyLib&lt;/code&gt; from day one—even if its initial &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__using__/1&lt;/code&gt; implementation is just an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import&lt;/code&gt;—you establish an extensible contract. You reserve the right to add compile-time hooks, behaviours, and setup logic in future releases without breaking a single line of client code.&lt;/p&gt;&lt;hr/&gt;&lt;h2&gt;7. Avoid Macros&lt;/h2&gt;&lt;p&gt;“Macros are harder to write… clear code is better than concise code… macros should only be used as a last resort.”&lt;/p&gt;&lt;p&gt;If the Elixir core team took this rule seriously, we wouldn’t have Ecto schemas, Phoenix routers, ExUnit tests. If I did, we wouldn’t have my libraries at all. If &lt;a href=&quot;https://hexdocs.pm/finitomata&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Finitomata&lt;/code&gt;&lt;/a&gt; can be probably rewritten in non-macro approach, &lt;a href=&quot;https://hexdocs.pm/telemetria&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Telemetría&lt;/code&gt;&lt;/a&gt; has zero chance to ever exist without all the spectre of macros, from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;use&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__before_compile__&lt;/code&gt; hooks and whatnot.&lt;/p&gt;&lt;p&gt;Trying to define complex domain logic using raw functions, maps, and anonymous function passing results in an unreadable wall of syntactical noise. Macros allow us to build expressive, declarative Domain-Specific Languages (DSLs). They perform compile-time validation, optimize data structures before execution, and turn complex domain semantics into intuitive code.&lt;/p&gt;&lt;p&gt;Without macros, Elixir would just be Erlang with different syntax. Embracing macros responsibly is what makes Elixir libraries powerful.&lt;/p&gt;&lt;hr/&gt;&lt;h2&gt;8. Avoid Using Processes for Code Organization&lt;/h2&gt;&lt;p&gt;The guidelines argue that processes must only model runtime properties (state, concurrency, fault tolerance) and never code organization, using the classic strawman of putting a basic calculator behind a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GenServer&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;While putting arithmetic in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GenServer&lt;/code&gt; is obviously silly, using processes as &lt;strong&gt;fault-isolation boundaries&lt;/strong&gt; for computation is a battle-tested OTP pattern.&lt;/p&gt;&lt;p&gt;Consider a library executing third-party NIFs, parsing untrusted image files, or running heavy memory-intensive computations. If you run that code directly inside the caller’s process and it triggers an out-of-memory error, segfaults, or enters an infinite loop, it takes down the caller.&lt;/p&gt;&lt;p&gt;Wrapping that “pure computation” inside a dedicated worker process isolates the failure. If the computation crashes, only the worker process dies; the caller receives an error tuple and survives. Processes are not just for state—they are hard memory and fault-containment zones.&lt;/p&gt;&lt;hr/&gt;&lt;h2&gt;9. Avoid Spawning Unsupervised Processes&lt;/h2&gt;&lt;p&gt;Finally, the guidelines insist that every process must live inside a supervision tree, warning against raw &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spawn/1&lt;/code&gt; or unsupervised tasks.&lt;/p&gt;&lt;p&gt;This ignores &lt;strong&gt;fire-and-forget side-effects&lt;/strong&gt; where coupling the task lifetime to the caller or supervisor is undesirable.&lt;/p&gt;&lt;p&gt;Take non-critical background operations: sending an asynchronous telemetry event, flushing an audit log, or dispatching a best-effort metric. If you start these tasks under the main caller’s supervision tree, a failure in the telemetry reporter can crash the supervisor or block the shutdown sequence of the primary application.&lt;/p&gt;&lt;p&gt;Spawning an unlinked, unsupervised task (or using a detached process) guarantees that the background side-effect runs independently on a best-effort basis without ever delaying, blocking, or crashing the primary business logic.&lt;/p&gt;&lt;hr/&gt;&lt;h2&gt;Summary&lt;/h2&gt;&lt;p&gt;The official Elixir library guidelines are a useful set of defaults for beginners, but defaults are not universal laws.&lt;/p&gt;&lt;p&gt;Every single “anti-pattern” in the guidelines exists because someone, at some point, needed to solve a real engineering problem that the “clean” abstraction couldn’t handle. Understanding &lt;em&gt;why&lt;/em&gt; a rule exists gives you the authority to break it when the trade-offs demand it.&lt;/p&gt;&lt;p&gt;Don’t let dogma dictate your architecture. Use application configuration when you need global defaults, use macros when you need expressive DSLs, use processes when you need fault isolation, and use the full power of the Erlang VM when your application calls for it.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>kmx.io blog : KC3 v0.1.17 released</title>
<link>https://www.kmx.io/blog/kc3-v0.1.17-released</link>
<enclosure type="image/jpeg" length="0" url="https://kmx.io/static/_images/kmx.logo.text.256.png"></enclosure>
<guid isPermaLink="false">p8KW5XXi-XHXl5nV0W75sQv1jIowcHaCFHYKgQ==</guid>
<pubDate>Mon, 03 Aug 2026 17:51:50 +0000</pubDate>
<description>Welcome to kmx.io</description>
<content:encoded>&lt;body&gt;
    

    &lt;main&gt;
      &lt;div&gt;
  
  &lt;span&gt;2026-08-02 20:44:18&lt;/span&gt;
  
  &lt;h1&gt;KC3 v0.1.17 released&lt;/h1&gt;
  &lt;p&gt;KC3 v0.1.17 was released today.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/kc3-lang/kc3/releases/tag/v0.1.17&quot;&gt;https://github.com/kc3-lang/kc3/releases/tag/v0.1.17&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://git.kmx.io/kc3-lang/kc3/_tree/v0.1.17&quot;&gt;https://git.kmx.io/kc3-lang/kc3/_tree/v0.1.17&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;New in this release :&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;libkc3&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;copying temporary variables for &lt;code&gt;pthread_mutex&lt;/code&gt; and &lt;code&gt;pthread_rwlock&lt;/code&gt;
would cause undefined behaviour as per POSIX, this fixes pthread on
macOS&lt;/li&gt;
&lt;li&gt;allow for log hooks (a C function callback with a user pointer)
in the facts database&lt;/li&gt;
&lt;li&gt;incremental compilation with cached parser results in
&lt;code&gt;.kc3c&lt;/code&gt; files, like Python does. Gives &lt;strong&gt;5x faster loading times&lt;/strong&gt;
for all &lt;code&gt;.kc3&lt;/code&gt; files. &lt;code&gt;env_load&lt;/code&gt; automatically handles this.&lt;/li&gt;
&lt;li&gt;fixed a bug in &lt;code&gt;ht_iterator_next&lt;/code&gt; where the iterator would not
go through the first collision list&lt;/li&gt;
&lt;li&gt;database logging now supports and defaults to binary format
(marshall + marshall_read)&lt;/li&gt;
&lt;li&gt;fixed marshall + marshall_read hash table usage&lt;/li&gt;
&lt;li&gt;str: fixed display of floating point numbers&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Facts&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Facts.connect/accept allows for bi-directional synchronization of
an existing &lt;code&gt;Facts.database()&lt;/code&gt; over a TLS encrypted connection
after a successful HMAC-SHA256 shared secret authentication
challenge/response&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Facts.accept&lt;/code&gt; accepts connections one by one&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Facts.acceptor_loop&lt;/code&gt; starts a thread that calls &lt;code&gt;accept()&lt;/code&gt;
in a loop&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Facts.acceptor_loop_join()&lt;/code&gt; stops the acceptor loop cleanly&lt;/li&gt;
&lt;li&gt;Fixed hash table lookup in marshall reducing every dump by 30%&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;HTTPd&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;allow for configuration of unveil paths in &lt;code&gt;config/unveil.kc3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;allow for custom log messages in error and request log&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;HTTPS&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HTTPS.Client&lt;/code&gt; with libtls and automatic or manual connection&lt;ul&gt;
&lt;li&gt;GET method&lt;/li&gt;
&lt;li&gt;POST method&lt;/li&gt;
&lt;li&gt;JSON response&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;JSON&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fixed parser&lt;ul&gt;
&lt;li&gt;boolean &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt; → Bool&lt;/li&gt;
&lt;li&gt;map &lt;code&gt;{&amp;quot;key&amp;quot;, &amp;quot;value&amp;quot;}&lt;/code&gt; → Map &lt;code&gt;%{&amp;quot;key&amp;quot; =&amp;gt; &amp;quot;value&amp;quot;}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;array &lt;code&gt;[1, 2, 3]&lt;/code&gt; → List &lt;code&gt;[1, 2, 3]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;RUsage&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;current-process statistics for user and system CPU time, current and
maximum resident memory, virtual memory, and open files&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RUsage.stats()&lt;/code&gt; returns all statistics in a single map&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

  &lt;div&gt;
    
    
  &lt;/div&gt;
&lt;/div&gt;

    &lt;/main&gt;
    &lt;div&gt;
  &lt;div&gt;
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    

    
    
    
    
    
    
    
    
    
    
    
    
    
    
    

    
    
    
    
    
    
    
    
    
    
    
    
    
    
    

    
    
    
    
    
    
    
    
    
    
    
    
    
    
    

    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
  &lt;/div&gt;
&lt;/div&gt;
&lt;footer&gt;
  &lt;div&gt;
    &lt;a href=&quot;https://www.kmx.io/&quot;&gt; Copyright 2020-2026 kmx.io&lt;/a&gt;    
    &lt;a href=&quot;https://www.kmx.io/contact&quot;&gt; Contact&lt;/a&gt;    
    &lt;a href=&quot;https://discord.gg/nUAr57YKsh&quot;&gt; Discord&lt;/a&gt;
    &lt;span&gt;
      &lt;a href=&quot;https://git.kmx.io/kc3-lang/kc3&quot;&gt;kc3_httpd&lt;/a&gt;
      &lt;a href=&quot;https://kc3-lang.org/release/v0.1.16/&quot;&gt;v0.1.16&lt;/a&gt;
    &lt;/span&gt;
    
  &lt;/div&gt;
&lt;/footer&gt;

  

&lt;/body&gt;</content:encoded>
</item>
<item>
<title>The notification that gave up after a minute | jvalol</title>
<link>https://jva.lol/weblog/the-notification-that-gave-up-after-a-minute/</link>
<enclosure type="image/jpeg" length="0" url="https://jva.lol/og/weblog/the-notification-that-gave-up-after-a-minute/card.png"></enclosure>
<guid isPermaLink="false">q7jIs3j_8uj49svtnBNy3mUPYFs1sP3GEuNlMg==</guid>
<pubDate>Mon, 20 Jul 2026 00:43:15 +0000</pubDate>
<description>Debugging a notification that stopped arriving after a minute: four healthy-looking layers, two quiet failures, one browser timeout.</description>
<content:encoded>&lt;h1&gt;&lt;a href=&quot;https://jva.lol/weblog/the-notification-that-gave-up-after-a-minute/&quot;&gt;The notification that gave up after a minute&lt;/a&gt;&lt;/h1&gt;&lt;div&gt;&lt;a href=&quot;https://jva.lol/categories/projects/&quot;&gt;Projects&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;a href=&quot;https://jva.lol/tags/popsicleboat/&quot;&gt;Popsicleboat&lt;/a&gt;,
&lt;a href=&quot;https://jva.lol/tags/web-push/&quot;&gt;Web-Push&lt;/a&gt;&lt;/div&gt;&lt;p&gt;&lt;time&gt;July 17, 2026&lt;/time&gt;&lt;/p&gt;&lt;p&gt;PopsicleBoat grew browser push notifications recently. The pitch is simple: hear when someone answers you, without handing anyone an email address. Your browser asks permission, a subscription goes to the server, and nothing personal leaves the device. I shipped it, watched the push service accept my first delivery with a tidy &lt;code&gt;201 Created&lt;/code&gt;, and felt good about the whole thing.&lt;/p&gt;&lt;p&gt;Then I replied to one of my own posts from my phone, looked at my laptop, and saw nothing.&lt;/p&gt;&lt;p&gt;No banner. No chime. The reply was on the site, the inbox badge lit up, the notification email arrived — every channel working except the one I’d just built. And here’s what made it a good puzzle: every layer of the push stack, inspected on its own, reported success.&lt;/p&gt;&lt;p&gt;The server sent the notification. The push service accepted it. The subscription was in the database. The service worker was registered. Four green lights, zero banners.&lt;/p&gt;&lt;p&gt;Web push is a relay race with four runners: your server signs a message and hands it to a push service (Google’s, for Chromium browsers), the push service holds it for the browser, the browser wakes a service worker, and the service worker asks the operating system to draw a banner. A dropped baton anywhere shows up the same way — silence at the finish line — and no runner files a report.&lt;/p&gt;&lt;p&gt;The first dropped baton was mine to find in the browser console. &lt;code&gt;Notification.permission&lt;/code&gt; said &lt;code&gt;&amp;quot;default&amp;quot;&lt;/code&gt;. Not &lt;code&gt;&amp;quot;granted&amp;quot;&lt;/code&gt;, not &lt;code&gt;&amp;quot;denied&amp;quot;&lt;/code&gt; — &lt;em&gt;never asked&lt;/em&gt;. Somewhere between enabling notifications and testing them, the site’s permission had ended up back at square one — a site-data sweep, probably — while the subscription it had authorized lived on in my database, perfectly valid, pointing at a browser that would no longer show anything. In that state, a page calling &lt;code&gt;new Notification()&lt;/code&gt; doesn’t error. It does nothing, silently, which is a bold choice for an API whose entire job is being noticed.&lt;/p&gt;&lt;p&gt;One re-grant later (“forever,” this time), banners worked. Victory lasted about an hour, until I noticed pushes still vanished whenever the browser wasn’t running.&lt;/p&gt;&lt;p&gt;That was the second baton, and it was hiding in a default I’d never questioned. Push services will happily hold a message for a browser that’s closed — that’s the whole point of the relay — but only as long as the message’s &lt;em&gt;time-to-live&lt;/em&gt; allows. The library I use sets that TTL to sixty seconds unless told otherwise. Sixty seconds. Close your laptop, get a reply two minutes later, and the push service shrugs and discards it. The &lt;code&gt;201&lt;/code&gt; it returned was entirely honest: message accepted. Nobody promised &lt;em&gt;kept&lt;/em&gt;.&lt;/p&gt;&lt;p&gt;The fix is one option:&lt;/p&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;send_notification(payload, message, ttl: 86_400)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A day. That’s roughly how long “someone answered you” stays worth interrupting someone for; any older and it’s the inbox’s job, which never forgets.&lt;/p&gt;&lt;p&gt;Two lessons sailed home with this one. First: in a layered system where every layer answers “OK,” the bug lives in the gaps between the layers — the permission that reverted between grant and use, the message that expired between accepted and delivered. Debugging meant walking the relay in order and asking each runner not “did you succeed?” but “what did you hand the next runner, and did anyone catch it?”&lt;/p&gt;&lt;p&gt;Second: defaults are decisions someone else made about your product. A conservative browser permission model, a sixty-second TTL — both defensible choices, by people who’d never seen my notification settings page and its promise to &lt;em&gt;hear when someone answers you&lt;/em&gt;. Keeping that promise meant finding every default standing between the reply and the banner, and overruling the ones that disagreed with it.&lt;/p&gt;&lt;p&gt;The banners arrive now. Even the morning after.&lt;/p&gt;&lt;img src=&quot;https://jva.lol/favicon.svg&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;</content:encoded>
</item>
<item>
<title>Notes from the dead letter box - Specifying Software for Teams and Agents</title>
<link>https://bitcrowd.dev/specifying-software-for-teams-and-agents</link>
<guid isPermaLink="false">Dp8gsj0Ahp5ucuJ_TdYvALWjXtDA2WkpAfnv7A==</guid>
<pubDate>Sun, 05 Jul 2026 12:34:08 +0000</pubDate>
<description>Specifying software for coding teams and agents: what changes, what stays the same, and how to write specs that both humans and AI agents can build from.</description>
<content:encoded>&lt;h2&gt;How to describe an application?&lt;a href=&quot;https://bitcrowd.dev/specifying-software-for-teams-and-agents#how-to-describe-an-application&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Imagine the following: The dev team receives the envelope with the application
specs from a dead letter box in the park at night. Two months later, they
deliver the code. Their client deploys their app, and everything works as
expected.&lt;/p&gt;&lt;h2&gt;The question: What was in that envelope?&lt;a href=&quot;https://bitcrowd.dev/specifying-software-for-teams-and-agents#the-question-what-was-in-that-envelope&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Product managers, engineers, and procurement teams have tried to deliver this
perfect blueprint for a software product. The ones that did not fail have mostly
succeeded by sheer luck. After decades of engineering, there is still no
commonly accepted standard for software requirements that guarantees, when
followed, the desired outcome. There is
&lt;a href=&quot;https://standards.ieee.org/ieee/29148/6937/&quot;&gt;IEEE 29148&lt;/a&gt;, but that carefully
avoids specifying the how: It tells you a requirement must be unambiguous and
verifiable, but not how to express it so that it actually is.&lt;/p&gt;&lt;p&gt;We were faced with this problem while we were working on
&lt;a href=&quot;https://surveyor.bitcrowd.ai/&quot;&gt;Surveyor&lt;/a&gt;, a tool to extract specifications from
running (legacy) systems: What is a good format for the description, once you
discovered its features?&lt;/p&gt;&lt;p&gt;Surveyor is currently in early access testing
(&lt;a href=&quot;https://73f4313d.sibforms.com/serve/MUIFAGf7xJuMm4W5YSsztruKlELH459zdPyB50IhmpgBnS6wyrTGqz7dQ55IJLSVa7CUPFvAHrUEvEYbHEn5tBivo8SQPN_cJIMdBr_O1xiQ9ug64k46sfNBIFuTHK1rNKqvqytuDkTUoh0C5XMyUKOPZy7whNQM8zviGihyCCrZYBTq7uTf5-35fK6Ki2YBOVnEAmV_WwrgoT1-wg==&quot;&gt;you can take part to&lt;/a&gt;),
and we already learned a lot during the first sessions applying it to other
peoples&amp;#39; legacy software. A good moment to share what we have found out, and to
ask for feedback. We have run analytics on our beta testers’ codebases, and here
is what has worked.&lt;/p&gt;&lt;h2&gt;The Premise&lt;a href=&quot;https://bitcrowd.dev/specifying-software-for-teams-and-agents#the-premise&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Our clandestine team needs freedom of choice. We don&amp;#39;t want to force them to use
&lt;code&gt;TINYINT&lt;/code&gt; if their favorite database does not support that. If an implementation
detail is important in a way that does not become clear from the context, we
need specify that in the tech choices, or describe the observable outcome in a
spec. Otherwise, we should aim to specify independent from technology /
solution. Here are our picks to do that.&lt;/p&gt;&lt;h2&gt;Architectural overview&lt;a href=&quot;https://bitcrowd.dev/specifying-software-for-teams-and-agents#architectural-overview&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;The &lt;a href=&quot;https://c4model.com/&quot;&gt;C4 model&lt;/a&gt; is a way to visualize software architecture
at four levels of abstraction, created by &lt;a href=&quot;https://simonbrown.je/&quot;&gt;Simon Brown&lt;/a&gt;.
The &amp;quot;C4&amp;quot; refers to its four diagram types, each zooming in further than the
last:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;System Context shows your system as a single box surrounded by the users and
external systems it interacts with. It answers &amp;quot;what does this system do and
who uses it&amp;quot; without any internal detail.&lt;/li&gt;&lt;li&gt;Containers zooms into the system to show its major deployable or runnable
pieces, like web apps, mobile apps, databases, APIs, and file systems, along
with how they communicate. Here &amp;quot;container&amp;quot; means a separately running unit,
not specifically a Docker container.&lt;/li&gt;&lt;li&gt;Components breaks a single container down into its main building blocks and
their responsibilities, showing how the code is organized into logical
groupings within that container.&lt;/li&gt;&lt;li&gt;Code is the most detailed level, but mostly unused or generated from the code.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The core idea is that you start broad and progressively zoom in, so different
audiences can engage at the level of detail that suits them, much like zooming
in on a map.&lt;/p&gt;&lt;p&gt;Our Beta testers have found these diagrams most helpful. Even engineers who have
worked with their codebase for years welcomed this organised visualisation.
Surveyor is generating C4 diagrams in the form of
&lt;a href=&quot;https://structurizr.com/&quot;&gt;Structurizr&lt;/a&gt; DSL.&lt;/p&gt;&lt;p&gt;This solves two purposes:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;It offers a standard for generating charts with a well known systematic&lt;/li&gt;&lt;li&gt;It can serve as an input for subsequent steps&lt;/li&gt;&lt;/ol&gt;&lt;h3&gt;C the fourth&lt;a href=&quot;https://bitcrowd.dev/specifying-software-for-teams-and-agents#c-the-fourth&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;The fourth C is “Code”, but it’s not often used as code changes are frequent.
Re-aligning UML diagrams each time the code changes adds friction we don’t want.
Instead, we leave the diagram level and move on to describing the behavior of
the system.&lt;/p&gt;&lt;h2&gt;Describing Behavior&lt;a href=&quot;https://bitcrowd.dev/specifying-software-for-teams-and-agents#describing-behavior&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;In 2006, a movement called
&lt;a href=&quot;https://cucumber.io/docs/bdd/&quot;&gt;Behaviour-Driven Development&lt;/a&gt; (BDD) emerged. The
idea was to write specifications in natural language that could be parsed and
executed as tests simultaneously. This enabled product teams to create
specifications that engineers could use directly as automated tests. The
engineers would start by creating the bindings between feature language and test
code. The implementation would begin with a failing test (red), which the actual
feature code would then make pass (green).&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://cucumber.io/docs/gherkin/&quot;&gt;Gherkin&lt;/a&gt; is a plain-text language for
writing executable specifications in a Given-When-Then format,
&lt;a href=&quot;https://cucumber.io/&quot;&gt;Cucumber&lt;/a&gt; is the framework that maps these specifications
to code (step definitions) so they run as automated tests.&lt;/p&gt;&lt;p&gt;A feature like the following:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;Feature: Monster battle

  Scenario: Battle

    Given there is a monster

    When I attack it

    Then it should die&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Would map to a binding like this:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;# test/features/step_definitions/monster_steps.exs
defmoduleMonsterStepsdo
useCucumber.StepDefinition
importExUnit.Assertions

  step &amp;quot;there is a monster&amp;quot;, context do
Map.put(context,:monster,Monster.new())
end

  step &amp;quot;I attack it&amp;quot;, context do
Map.put(context,:monster,Monster.take_hit(context.monster))
end

  step &amp;quot;it should die&amp;quot;, context do
    refute context.monster.alive?
    context
end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;BDD’s peak popularity came in the early 2010s, a period when teams worldwide
embraced it and equivalents appeared in every major language with
implementations like &lt;a href=&quot;https://behave.readthedocs.io/&quot;&gt;Behave&lt;/a&gt; and
&lt;a href=&quot;https://pytest-bdd.readthedocs.io/&quot;&gt;pytest-bdd&lt;/a&gt; for Python,
&lt;a href=&quot;https://specflow.org/&quot;&gt;SpecFlow&lt;/a&gt;/&lt;a href=&quot;https://reqnroll.net/&quot;&gt;Reqnroll&lt;/a&gt; for .NET,
&lt;a href=&quot;https://docs.behat.org/&quot;&gt;Behat&lt;/a&gt; for PHP, and
&lt;a href=&quot;https://github.com/cucumber/godog&quot;&gt;Godog&lt;/a&gt; for Go.&lt;/p&gt;&lt;p&gt;Tooling is broad too, including IDE plugins for
&lt;a href=&quot;https://www.jetbrains.com/idea/&quot;&gt;IntelliJ&lt;/a&gt;,
&lt;a href=&quot;https://visualstudio.microsoft.com/&quot;&gt;Visual Studio&lt;/a&gt;, and
&lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;VS Code&lt;/a&gt; offering syntax highlighting and step
navigation, plus reporting tools and CI/CD integrations&lt;/p&gt;&lt;p&gt;Interest has cooled down some years after that because developers grew tired of
maintaining large chunks of BDD code and bindings. Luckily, we can leave that
part to the LLMs now.&lt;/p&gt;&lt;h3&gt;The Case for BDD&lt;a href=&quot;https://bitcrowd.dev/specifying-software-for-teams-and-agents#the-case-for-bdd&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;We are revisiting BDD today, because AI assisted coding has a problem: Plans and
free formats like &lt;a href=&quot;https://openspec.pro/&quot;&gt;OpenSpec&lt;/a&gt; are not parsable. That means
they can not be translated to automated tests directly. You need an agent step
to build the testing, with all uncertainty agentic coding brings.&lt;/p&gt;&lt;p&gt;The relatively rigid syntax of Gherkin is an advantage here: It maps
mechanically to test code. And it can also be parsed to produce other output
formats: Manual QA instructions, OpenSpec or
&lt;a href=&quot;https://www.atlassian.com/software/jira&quot;&gt;Jira&lt;/a&gt; tickets.&lt;/p&gt;&lt;p&gt;But for that, it’s missing the meta information that embeds a feature
description into its application context.&lt;/p&gt;&lt;h3&gt;The Assay format&lt;a href=&quot;https://bitcrowd.dev/specifying-software-for-teams-and-agents#the-assay-format&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;We have used the C4/structurizr DSL to express how our system interfaces with
the outside world. We also used it to describe the inner composition of our
application. When we use Gherkin alone, this context would be lost. But we can
preserve it with syntax-aware comments like this:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;# ---
# component: orderLifecycle
# container: api
# schema: schemas/order_lifecycle.ex
# workspace: architecture/workspace.tsp
# definitions:
#   a valid customer:
#     a Customer with status Active, verified email,
#     and a credit limit greater than zero
#   in stock:
#     the product has available quantity greater than
#     the requested amount
# invariants:
#   - total must not exceed customer credit limit
#   - at least one line item required
#   - cannot cancel an order that has shipped
# ---&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;With this, we can access the information in the structurizr DSL to get context
information. This allows us to write parsers and plugins for coding agents that
can not only read the features, but also access context information we gathered
before. But what&amp;#39;s purpose of the &lt;code&gt;schema:&lt;/code&gt; definition?&lt;/p&gt;&lt;h2&gt;Schema Definitions&lt;a href=&quot;https://bitcrowd.dev/specifying-software-for-teams-and-agents#schema-definitions&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Schema work as a shared contract about the business objects of a component.
Having them speeds up onboarding. Developers don&amp;#39;t need to scan through all
features to detect what fields are needed, and agents are less likely to invent
fields. It&amp;#39;s a token-efficient way to inject exactly the ground truth into
context.&lt;/p&gt;&lt;p&gt;For specifying schemas, three obvious candidates come to mind:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Protobuf — &lt;a href=&quot;https://protobuf.dev/&quot;&gt;https://protobuf.dev&lt;/a&gt; — Google&amp;#39;s schema and wire format. Compact and
fast, with code generators across many languages, but its scalar types are
physical commitments, so it presumes the most about storage and encoding.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;syntax = &amp;quot;proto3&amp;quot;;

message Post {
  string title = 1;
  int32 views = 2;
  bool published = 3;
  repeated string tags = 4;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;JSON Schema — &lt;a href=&quot;https://json-schema.org/&quot;&gt;https://json-schema.org&lt;/a&gt; — A decade-plus standard for describing
the shape and constraints of JSON. Stays loose where you want, needs no
toolchain for consumers, and fits the premise most directly; the cost is
verbosity. It can be authored directly, e.g. as YAML and be compiled to JSON:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;$schema:&amp;quot;https://json-schema.org/draft/2020-12/schema&amp;quot;
$id:&amp;quot;https://assay.example/post&amp;quot;
title: Post
type: object
required:[title]
additionalProperties:false
properties:
title:{type: string }
views:{type: integer }
published:{type: boolean }
tags:
type: array
items:{type: string }&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;TypeSpec — &lt;a href=&quot;https://typespec.io/&quot;&gt;https://typespec.io&lt;/a&gt; — Microsoft&amp;#39;s TypeScript-like language that
compiles to JSON Schema, OpenAPI, and Protobuf from one source. Compact and
readable, at the price of a build step and a single-vendor toolchain.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;model Post {
  title:string;
  views?: integer;
  published?:boolean;
  tags?:string[];
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Under the premise that the implementing team picks the field types and specifics
the three differ in how much they presume. Protobuf fits worst: its scalar types
(int32, bytes) are physical commitments that pre-decide the storage details you
meant to leave open. JSON Schema is the opposite — it describes shape and
constraints without binding to any storage type, and hands the team a
directly-usable artifact with no toolchain; its cost is verbosity and $ref-wired
files. For our purpose, TypeSpec is the golden spot:&lt;/p&gt;&lt;p&gt;A compact, TypeScript-like authoring that compiles to JSON Schema (or Protobuf
later, if needed), at the price of a build step and a younger, single-vendor
toolchain. This is not so relevant in this context as we use it for
documentation rather than compiling. It adds, however, the cross compilation
benefits.&lt;/p&gt;&lt;h2&gt;Architectural Decision&lt;a href=&quot;https://bitcrowd.dev/specifying-software-for-teams-and-agents#architectural-decision&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Until this point, we avoided to describe the “how” of our application. Which
database, which web framework, what datamodel. This is what
&lt;a href=&quot;https://adr.github.io/madr/&quot;&gt;Markdown Architectural Decision Records&lt;/a&gt; (MADR)
cover. Their goal is to document not only the technical decision, but what
options were considered and what the problem statement was. Because MADRs
contain that background, it allows to effectively reconsider decisions once the
Circumstances have changed.&lt;/p&gt;&lt;p&gt;They typically look like this:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;## **Use PostgreSQL for primary data store**

* Status: accepted
* Date: 2026-06-29

### **Context**

We need a primary database for a new service with relational data and strong
consistency requirements.

### **Considered Options**

* PostgreSQL
* MongoDB

### **Decision**

Use **PostgreSQL** — the team has operational experience with it, and it
provides ACID transactions plus JSONB for flexible fields. MongoDB was rejected
due to weaker consistency guarantees and less team familiarity.

### **Consequences**

* Good: mature ecosystem, existing team expertise
* Bad: harder to scale horizontally than MongoDB&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;h2&gt;Non Functional Requirements&lt;a href=&quot;https://bitcrowd.dev/specifying-software-for-teams-and-agents#non-functional-requirements&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;By now we have described our application well in terms of how it is structured,
what it does, and what tools it uses (or should use). But we left out the
quality of service.&lt;/p&gt;&lt;p&gt;A non-functional requirement (NFR) specifies how well a system must operate, as
opposed to what it does (functional requirements). Where a functional
requirement says &amp;quot;users can book an appointment,&amp;quot; an NFR constrains the
qualities of that behavior — performance, scalability, availability,
reliability, security, maintainability, usability, and so on.&lt;/p&gt;&lt;p&gt;They are often written in a simple table format:&lt;/p&gt;&lt;p&gt;Here&amp;#39;s a fuller set, functional requirement paired with its non-functional
counterpart:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Quality attribute&lt;/th&gt;&lt;th&gt;Functional requirement (&lt;em&gt;what&lt;/em&gt;)&lt;/th&gt;&lt;th&gt;Non-functional requirement (&lt;em&gt;how well&lt;/em&gt;)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Performance&lt;/td&gt;&lt;td&gt;User searches for available slots&lt;/td&gt;&lt;td&gt;Results return in under 500ms for 95% of requests&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Scalability&lt;/td&gt;&lt;td&gt;Users book appointments&lt;/td&gt;&lt;td&gt;System handles 1,000 concurrent bookings without degradation&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Availability&lt;/td&gt;&lt;td&gt;The booking system is accessible&lt;/td&gt;&lt;td&gt;99.9% uptime, measured monthly (≈43 min downtime/month)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Reliability&lt;/td&gt;&lt;td&gt;A confirmed booking is saved&lt;/td&gt;&lt;td&gt;Zero confirmed bookings lost; durable before confirmation returns&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Security&lt;/td&gt;&lt;td&gt;Users log in to their account&lt;/td&gt;&lt;td&gt;Passwords stored hashed; data encrypted in transit (TLS) and at rest&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Usability&lt;/td&gt;&lt;td&gt;User completes a booking&lt;/td&gt;&lt;td&gt;A new user can book without instructions in under 2 minutes&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;Each row shows the same feature twice: the left column is &lt;em&gt;what the system
does&lt;/em&gt;, the right is the measurable constraint on &lt;em&gt;how well it must do it&lt;/em&gt; — the
NFR.&lt;/p&gt;&lt;h2&gt;The End-Boss: Design&lt;a href=&quot;https://bitcrowd.dev/specifying-software-for-teams-and-agents#the-end-boss-design&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Specifying UI is a daunting task because it happens at the end of the rendering
pipeline where the amount of moving parts is highest. Most tools for web
therefore use the web technologies themselves to avoid drifting.&lt;/p&gt;&lt;h3&gt;The imageto effectively reconsider decisions once the Circumstances have changed.&lt;a href=&quot;https://bitcrowd.dev/specifying-software-for-teams-and-agents#the-imageto-effectively-reconsider-decisions-once-the-circumstances-have-changed&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;The simplest, (and arguably the worst) medium to specify design is the image:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;Then the login page should match &amp;quot;fixtures/login.png&amp;quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This allows features to specify design alongside the functional feature
requirements.&lt;/p&gt;&lt;h3&gt;The code&lt;a href=&quot;https://bitcrowd.dev/specifying-software-for-teams-and-agents#the-code&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;If the frontend code is using components and maybe even a component library, it
might be worth carrying over. A connection to a design system in
&lt;a href=&quot;https://www.figma.com/&quot;&gt;Figma&lt;/a&gt; or similar is the gold standard here, but might
be violating our &amp;quot;letter in envelope&amp;quot; constraint. Our goto strategy at bitcrowd
is &lt;a href=&quot;https://storybook.js.org/&quot;&gt;Storybook&lt;/a&gt;. If the target system supports it,
teams can work very effectively.&lt;/p&gt;&lt;h3&gt;Visual Regression Testing&lt;a href=&quot;https://bitcrowd.dev/specifying-software-for-teams-and-agents#visual-regression-testing&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;If you happen to have a legacy system with a decent design, visual regression
testing with &lt;a href=&quot;https://playwright.dev/&quot;&gt;Playwright&lt;/a&gt; or similar might be your tool
of choice. It gives teams a quick feedback loop if it comes to single scenarios,
and the option to recheck the whole interface. The great thing in combination
with Gherkin is that you can switch screenshot processing on and off. The
difficulty with visual regression tests is their potential flakiness. However,
image processing models have made this a lot more reliable.&lt;/p&gt;&lt;h2&gt;So, what is in that envelope?&lt;a href=&quot;https://bitcrowd.dev/specifying-software-for-teams-and-agents#so-what-is-in-that-envelope&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;So far we have C4/structurizr diagrams for how the system is build and how it
interacts, Gherkin/Assay for its behaviour, TypeSpec for schemas, MADR for the
technological choices and NFR for its quality of service. We go for storybook
for the visuals. Obviously, this is not the final solution for every stack. It
would be hard to describe a pacemaker OS with it, or a web application with
pristine UI effects. The final version of Surveyor will use plugins to cater for
that, but for many of the projects bitcrowd has encountered, this gives a good
start.&lt;/p&gt;&lt;p&gt;Fortunately, we have clients who enjoy talking to us, so we haven’t had to
resort to espionage technology - yet. Let us know if you have some clandestine
system you need to build in the dark, or that legacy application needs a
rewrite.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>CORS in Phoenix</title>
<link>https://www.leighhalliday.com/cors-in-phoenix</link>
<guid isPermaLink="false">yAQHTJjlXOqux4lbU3DlMUFt9YNkQuV_hzR8Cw==</guid>
<pubDate>Tue, 30 Jun 2026 23:43:47 +0000</pubDate>
<description>Single page applications Single page applications (SPA) are becoming more and more popular, often replacing the more traditional server rendered websites that are common in Rails or PHP. You have options such as Angular, Ember, React, and there are…</description>
<content:encoded>Single page applications Single page applications (SPA) are becoming more and more popular, often replacing the more traditional server rendered websites that are common in Rails or PHP. You have options such as Angular, Ember, React, and there are…</content:encoded>
</item>
<item>
<title>Recursion in Elixir</title>
<link>https://www.leighhalliday.com/recursion-in-elixir</link>
<guid isPermaLink="false">J3kC6jDC5GSiVUN6fmpAiNkZzJumIVbcFRbzAg==</guid>
<pubDate>Tue, 30 Jun 2026 23:43:47 +0000</pubDate>
<description>Intro I recently wrote an article on Recursion in Ruby , and this is meant to be its Elixir counterpart. It will provide a way to compare solving the same problems in both languages and a chance to talk about some of their differences. Heads &amp; Tails…</description>
<content:encoded>Intro I recently wrote an article on Recursion in Ruby , and this is meant to be its Elixir counterpart. It will provide a way to compare solving the same problems in both languages and a chance to talk about some of their differences. Heads &amp;amp; Tails…</content:encoded>
</item>
<item>
<title>FizzBuzz in Elixir</title>
<link>https://www.leighhalliday.com/fizzbuzz-in-elixir</link>
<guid isPermaLink="false">HO-UjpgkZnc4LtTrF8nm_PciE3Vookfuj2x5Ng==</guid>
<pubDate>Tue, 30 Jun 2026 23:43:47 +0000</pubDate>
<description>Intro... learning Elixir This is my first post in the new Elixir category I&#39;ve set up on my site. I&#39;m new to Elixir and functional programming in general, aside from a Scala course I&#39;ve done on Coursera. Ruby is the language I&#39;m currently most…</description>
<content:encoded>Intro... learning Elixir This is my first post in the new Elixir category I&amp;#39;ve set up on my site. I&amp;#39;m new to Elixir and functional programming in general, aside from a Scala course I&amp;#39;ve done on Coursera. Ruby is the language I&amp;#39;m currently most…</content:encoded>
</item>
<item>
<title>Guards! Guards! - Hauleth</title>
<link>https://hauleth.dev/post/guards-guards/</link>
<enclosure type="image/jpeg" length="0" url="https://hauleth.dev/banner.png"></enclosure>
<guid isPermaLink="false">4sdfoRkNrcrnv7gs_aqz_26gNLAJFW_Y-Z1-VA==</guid>
<pubDate>Sat, 27 Jun 2026 16:31:10 +0000</pubDate>
<description>Small gotcha about boolean operators in Elixir.</description>
<content:encoded>&lt;header&gt;&lt;div&gt;&lt;div&gt;&lt;a href=&quot;https://hauleth.dev/&quot;&gt; &lt;div&gt;~hauleth&lt;/div&gt; &lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;/header&gt;&lt;div&gt;&lt;article&gt;&lt;h1&gt;&lt;a href=&quot;https://hauleth.dev/post/guards-guards/&quot;&gt;Guards! Guards!&lt;/a&gt;&lt;/h1&gt;&lt;div&gt;&lt;span&gt;&lt;time&gt;2026.06.27&lt;/time&gt;&lt;/span&gt; :: &lt;time&gt;2 min&lt;/time&gt; :: #&lt;a href=&quot;https://hauleth.dev/tags/beam/&quot;&gt;beam&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;Let&amp;#39;s start with simple quiz.&lt;/p&gt;&lt;hr/&gt;&lt;p&gt;Given module defined as:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule Foo do
  def a(x) when is_integer(x) or is_map_key(x, :foo), do: true
  def a(x), do: false

  def b(x) when is_map_key(x, :foo) or is_integer(x), do: true
  def b(x), do: false
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Try to answer these questions.&lt;/p&gt;&lt;div&gt;&lt;p&gt;&lt;b&gt;Q:&lt;/b&gt; What will be result of &lt;code&gt;Foo.a(%{foo: 21})&lt;/code&gt;?&lt;/p&gt;&lt;div&gt;&lt;code&gt;true&lt;/code&gt;&lt;/div&gt;&lt;div&gt;&lt;code&gt;false&lt;/code&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;WRONG&lt;/div&gt;&lt;div&gt;RIGHT&lt;/div&gt;&lt;p&gt;This one is straightforward.&lt;/p&gt;&lt;p&gt;We check guard, it has one condition &lt;code&gt;is_integer(x) or is_map_key(x, :foo)&lt;/code&gt;. First one returns &lt;code&gt;false&lt;/code&gt;, second returns &lt;code&gt;true&lt;/code&gt;, Boolean&amp;#39;s alternative results in &lt;code&gt;true&lt;/code&gt; and first case is matched.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;b&gt;Q:&lt;/b&gt; What will be result of &lt;code&gt;Foo.a(37)&lt;/code&gt;?&lt;/p&gt;&lt;div&gt;&lt;code&gt;true&lt;/code&gt;&lt;/div&gt;&lt;div&gt;&lt;code&gt;false&lt;/code&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;WRONG&lt;/div&gt;&lt;div&gt;RIGHT&lt;/div&gt;&lt;p&gt;This one is straightforward as well.&lt;/p&gt;&lt;p&gt;We check guard, it has one condition &lt;code&gt;is_integer(x) or is_map_key(x, :foo)&lt;/code&gt;. First one returns &lt;code&gt;true&lt;/code&gt;, second one isn&amp;#39;t fired at all, because &lt;code&gt;or&lt;/code&gt; operator is short circuiting.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;b&gt;Q:&lt;/b&gt; What will be result of &lt;code&gt;Foo.b(%{foo: 21})&lt;/code&gt;?&lt;/p&gt;&lt;div&gt;&lt;code&gt;true&lt;/code&gt;&lt;/div&gt;&lt;div&gt;&lt;code&gt;false&lt;/code&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;WRONG&lt;/div&gt;&lt;div&gt;RIGHT&lt;/div&gt;&lt;p&gt;Again, similar to the previous questions.&lt;/p&gt;&lt;p&gt;We check guard, it has one condition &lt;code&gt;is_map_key(x, :foo) or is_integer(x)&lt;/code&gt;. First one returns &lt;code&gt;true&lt;/code&gt; and the rest is short circuited.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;b&gt;Q:&lt;/b&gt; What will be result of &lt;code&gt;Foo.b(37)&lt;/code&gt;?&lt;/p&gt;&lt;div&gt;&lt;code&gt;true&lt;/code&gt;&lt;/div&gt;&lt;div&gt;&lt;code&gt;false&lt;/code&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;WRONG&lt;/div&gt;&lt;div&gt;RIGHT&lt;/div&gt;&lt;p&gt;Ouch, something changed…&lt;/p&gt;&lt;p&gt;Again, we check guard, one condition &lt;code&gt;is_map_key(x, :foo) or is_integer(x)&lt;/code&gt;. We hit first clause &lt;code&gt;is_map_key(x, :foo)&lt;/code&gt; and this &lt;strong&gt;doesn&amp;#39;t&lt;/strong&gt; return &lt;code&gt;false&lt;/code&gt;, instead it fail. Failure in one of guard functions isn&amp;#39;t converted to &lt;code&gt;false&lt;/code&gt; but instead makes whole guard expression fail. This mean that &lt;code&gt;is_integer(x)&lt;/code&gt; part will &lt;strong&gt;never&lt;/strong&gt; be called.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;hr/&gt;&lt;p&gt;This behaviour is often surprising for a lot of Elixir developers, as it seemingly breaks commutative property of boolean operators. However, to be honest, these never were commutative because of short circuiting.&lt;/p&gt;&lt;p&gt;It seems that Elixir at the time of writing (Elixir 1.20.1, OTP 29) do not warn about this issue.&lt;/p&gt;&lt;div&gt;∎&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;Author of this post is currently open for hire (&lt;a href=&quot;https://hauleth.dev/cv&quot;&gt;CV&lt;/a&gt;).&lt;/p&gt;&lt;p&gt;You can contact me at &lt;a href=&quot;https://hauleth.dev/post/guards-guards/lukasz@niemier.pl&quot;&gt;lukasz@niemier.pl&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&lt;hr/&gt;&lt;div&gt;&lt;p&gt;You can provide feedback via mailing list &lt;a href=&quot;mailto:~hauleth/blog@lists.sr.ht?subject=[Comment] Guards! Guards!&quot;&gt;~hauleth/blog@lists.sr.ht&lt;/a&gt; (&lt;a href=&quot;https://lists.sr.ht/~hauleth/blog&quot;&gt;archive&lt;/a&gt;).&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;Webmentions:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://bsky.app/profile/did:plc:6psaz6n5fuurrfet67zs4ljf/post/3mpbudsxwi22h&quot;&gt;https://bsky.app/profile/did:plc:6psaz6n5fuurrfet67zs4ljf/post/3mpbudsxwi22h&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/div&gt;&lt;/article&gt;&lt;/div&gt;&lt;footer&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;copyright by &lt;a href=&quot;https://hauleth.dev&quot;&gt;hauleth&lt;/a&gt;&lt;/div&gt;&lt;div&gt;public tracking available at &lt;a href=&quot;https://plausible.io/hauleth.dev&quot;&gt;Plausible.io&lt;/a&gt;&lt;/div&gt;&lt;div&gt;&lt;a href=&quot;https://tangled.sh/hauleth.dev/blog&quot;&gt;source code&lt;/a&gt;&lt;/div&gt;&lt;div&gt;proudly hosted in 🇪🇺 via &lt;a href=&quot;https://statichost.eu&quot;&gt;StaticHost.eu&lt;/a&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;details&gt;&lt;summary&gt;Webrings&lt;/summary&gt; &lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://beambloggers.com//prev?referrer=https://hauleth.dev&quot;&gt;«&lt;/a&gt; &lt;a href=&quot;https://beambloggers.com/&quot;&gt;Beambloggers&lt;/a&gt; &lt;a href=&quot;https://beambloggers.com//next?referrer=https://hauleth.dev&quot;&gt;»&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/details&gt;&lt;/div&gt;&lt;/footer&gt;</content:encoded>
</item>
<item>
<title>This week in infrastructure</title>
<link>https://theconsensus.dev/p/2026/06/11/this-week-in-infrastructure.html</link>
<guid isPermaLink="false">m4384WW32NJsreyacyL7Ww5o4rGQj4QhFfiPhw==</guid>
<pubDate>Sun, 21 Jun 2026 17:34:48 +0000</pubDate>
<description>Supabase announces v0.1 of a distributed Postgres project, Multigres. VillageSQL announces a REST layer for MySQL a la PostgREST. MariaDB embeds DuckDB. Elixir is now gradually typed. PgDog gets funded. Native Linux containers for macOS reaches 1.0. And a new Cassandra client written in Rust.</description>
<content:encoded>&lt;p&gt;Supabase announces v0.1 of a distributed Postgres project, Multigres. VillageSQL announces a REST layer for MySQL a la PostgREST. MariaDB embeds DuckDB. Elixir is now gradually typed. PgDog gets funded. Native Linux containers for macOS reaches 1.0. And a new Cassandra client written in Rust.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Exploring Programming Languages</title>
<link>https://rocket-science.ru/hacking/2026/06/17/programming-languages</link>
<enclosure type="image/jpeg" length="0" url="https://rocket-science.ru/img/logo/logo-orig.png"></enclosure>
<guid isPermaLink="false">I-hd2LBCOKMOJ49cN7a-yP_hgbeZMl7Dr-EX3A==</guid>
<pubDate>Fri, 19 Jun 2026 18:28:26 +0000</pubDate>
<description>True story of the developer exploring different programming languages</description>
<content:encoded>&lt;div&gt;&lt;a href=&quot;https://soundcloud.com/nott-lovland&quot;&gt;Nott Løvland&lt;/a&gt; · &lt;a href=&quot;https://soundcloud.com/nott-lovland/exploring-languages&quot;&gt;Exploring Languages&lt;/a&gt;&lt;/div&gt;&lt;p&gt;In the beginning, I sought to be wise,&lt;br/&gt;
with a language that opened my digital eyes.&lt;br/&gt;
I started with LISP, for the pure of the mind,&lt;br/&gt;
But spent three whole weeks trying pairs to unwind.&lt;/p&gt;&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(((((((Are (we) (sure) (this) (is) (right?))))))))&lt;/code&gt;&lt;br/&gt;
Parentheses blinded my aching eyesight.&lt;/p&gt;&lt;p&gt;So I fled to the past, where the money was made,&lt;br/&gt;
and woke up in COBOL, deeply afraid.&lt;br/&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IDENTIFICATION DIVISION&lt;/code&gt; roared in my head,&lt;br/&gt;
with columns and margins, I wished I were dead.&lt;br/&gt;
It’s great for a bank in the year seventy-nine,&lt;br/&gt;
But a thousand lines later, I still couldn’t sign.&lt;/p&gt;&lt;p&gt;“Let’s try something hip!” I exclaimed with a twirl,&lt;br/&gt;
and drowned in a bucket of regex and Perl.&lt;br/&gt;
A script that looked exactly like line-noise and spit,&lt;br/&gt;
It ran, but God help me, I can’t read a bit.&lt;br/&gt;
Is that a variable, or did my cat walk&lt;br/&gt;
across the keyboard while trying to talk?&lt;/p&gt;&lt;p&gt;Then came Python, the savior, the clean, and the bright!&lt;br/&gt;
Until a stray space ruined my day and my night.&lt;br/&gt;
“Indentation Error,” the compiler did shriek,&lt;br/&gt;
because of one tab in the middle of the week.&lt;br/&gt;
Don’t get me even started on packaging hell,&lt;br/&gt;
where pip and venv cast a curse and a spell.&lt;/p&gt;&lt;p&gt;I jumped into Ruby, for joy and for love,&lt;br/&gt;
with blocks and with gems sent from heaven above.&lt;br/&gt;
But &lt;em&gt;Monkey Patching&lt;/em&gt; turned the code to a zoo,&lt;br/&gt;
When a library changed what &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2 + 2&lt;/code&gt; do.&lt;br/&gt;
It was beautiful, sure, but it ran like a snail,&lt;br/&gt;
Chugging along on a rusted old rail.&lt;/p&gt;&lt;p&gt;So I went corporate. Enter Java, the grand.&lt;br/&gt;
The boilerplate king of the enterprise land!&lt;br/&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AbstractMethodFactoryProviderBean&lt;/code&gt;&lt;br/&gt;
—was the shortest class name that I ever had seen.&lt;br/&gt;
I typed until my fingers were bleeding and numb,&lt;br/&gt;
just to print out “Hello” to a world that was glum.&lt;/p&gt;&lt;p&gt;“To the web!” I declared, and embraced JavaScript,&lt;br/&gt;
where logic is warped and reality’s flipped.&lt;br/&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[] == ![]&lt;/code&gt; evaluated to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;true&lt;/code&gt;,&lt;br/&gt;
and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NaN&lt;/code&gt; is a number? I’m sorry, that’s rude!&lt;br/&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;undefined&lt;/code&gt; is not a function, it cried in my face,&lt;br/&gt;
as npm bloated and swallowed my disk space.&lt;/p&gt;&lt;p&gt;So I looked for speed, and Golang caught my eye,&lt;br/&gt;
“It’s simple!” they promised, “Just give it a try!”&lt;br/&gt;
But &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if err != nil&lt;/code&gt; was on every damn line,&lt;br/&gt;
an endless repetition of error design.&lt;br/&gt;
No generics (at first), just copy and paste,&lt;br/&gt;
a minimalist’s dream, and a developer’s waste.&lt;/p&gt;&lt;p&gt;Then Rust was the answer, the savior of tech!&lt;br/&gt;
If the Borrow Checker didn’t snap my poor neck.&lt;br/&gt;
“You can’t use lifetimes! This memory’s owned!”&lt;br/&gt;
I sat at my desk, thoroughly powned.&lt;br/&gt;
I fought with the compiler for hours on end,&lt;br/&gt;
until I forgot why I coded, my friend.&lt;/p&gt;&lt;p&gt;Then out of the ashes, a phoenix arose,&lt;br/&gt;
embraced by Erlang, in functional prose.&lt;br/&gt;
Elixir! Oh, sweet elixir of life,&lt;br/&gt;
you banished my sorrow, you ended my strife.&lt;br/&gt;
With pattern matching so clean and so neat,&lt;br/&gt;
and pipe operators (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;|&amp;gt;&lt;/code&gt;) that make life a treat.&lt;br/&gt;
The BEAM handles millions of actors with grace,&lt;br/&gt;
while I sit with a massive, smug smile on my face.&lt;/p&gt;&lt;p&gt;Let the servers all crash! Let the supervisors play!&lt;br/&gt;
I’m finally happy. Go away, anyway.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Elixir for a Bluesky DataPlane: the choice we didn&#39;t expect</title>
<link>https://bitcrowd.dev/why-elixir-bluesky-dataplane</link>
<guid isPermaLink="false">PPIi53N8Uk9a7v2fua6aPKkXFhgOr__Zl3jHfg==</guid>
<pubDate>Thu, 18 Jun 2026 20:30:01 +0000</pubDate>
<description>Why we chose Elixir over Go, Rust and Node for a high-performance Bluesky DataPlane - and how a small Rust NIF and in-process fan-out made it the right fit.</description>
<content:encoded>&lt;p&gt;Bluesky&amp;#39;s source code is widely open source, so you can run your own social
network with it. - Provided you stay with a comparably small user base. What&amp;#39;s
missing? A performant DataPlane implementation. Closing this gap would be an
important step towards building digital independence.&lt;/p&gt;&lt;p&gt;We wanted to contribute our share and decided to work on a performant DataPlane
for Bluesky. When we started the project, we expected to work in Go, Rust or
even Node. After all, these were the predominant languages in the community.
Instead, we landed with Elixir. Here is why and how we came to that decision.&lt;/p&gt;&lt;h2&gt;The component nobody knows about&lt;a href=&quot;https://bitcrowd.dev/why-elixir-bluesky-dataplane#the-component-nobody-knows-about&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Everyone who has looked into the Bluesky infrastructure knows about the Personal
Data Stores (PDS), the Relay or the AppView. When you set up your own little
Bluesky from the
&lt;a href=&quot;https://github.com/bluesky-social/atproto&quot;&gt;ATProto repository&lt;/a&gt;, you run the
same components as Bluesky, the commercial service. However, there&amp;#39;s one notable
exception: the &lt;em&gt;DataPlane&lt;/em&gt;, a part of the AppView. It exists publicly only as a
Node-and-Postgres reference implementation, while the production Bluesky network
runs on a dedicated, &lt;a href=&quot;https://www.scylladb.com/&quot;&gt;ScyllaDB&lt;/a&gt;-backed, closed-source
DataPlane (as documented in
&lt;a href=&quot;https://newsletter.pragmaticengineer.com/i/114113498/5-scaling-the-database-layer&quot;&gt;Pragmatic Engineer&amp;#39;s deep-dive on Bluesky&amp;#39;s architecture&lt;/a&gt;).&lt;/p&gt;&lt;p&gt;That gap is exactly where things get interesting. The reference implementation
tells you what the DataPlane &lt;em&gt;does&lt;/em&gt;; it doesn&amp;#39;t tell you how to make it survive
contact with real traffic.&lt;/p&gt;&lt;p&gt;If you want to run your own, you have to answer the scaling question yourself -
and the first step is understanding the workload well enough to stop treating it
as one thing.&lt;/p&gt;&lt;figure&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/bluesky-5fcedfef78dd330f8c008f30b0457a02.png&quot; alt=&quot;Bluesky architecture overview&quot; title=&quot;&quot;/&gt;&lt;figcaption&gt;The DataPlane is a central component of the AppView in Bluesky&amp;#39;s architecture. Read the&lt;a href=&quot;https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto&quot;&gt;previous post&lt;/a&gt;to learn more.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;ScyllaDB is Bluesky&amp;#39;s operational choice, not part of the interface. The
DataPlane&amp;#39;s contract is a gRPC service that answers high-volume, low-complexity
queries and returns &lt;em&gt;skeletons&lt;/em&gt; - lists of IDs, counts, booleans - which a
higher layer later hydrates into full views. What sits behind that contract is
entirely up to you: the language, and the datastore. So before picking either,
we spent our time on the only thing that actually constrains the choice: the
shape of the load.&lt;/p&gt;&lt;h2&gt;A tale of two workloads&lt;a href=&quot;https://bitcrowd.dev/why-elixir-bluesky-dataplane#a-tale-of-two-workloads&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;The Bluesky&amp;#39;s historical data is enormous — terabytes. But when a user opens the
app, they read a few dozen recent posts, get distracted, and wander off to a
profile or a thread. They almost never scroll back further than a day or two.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;A note on specifics:
&lt;a href=&quot;https://jazco.dev/2025/02/19/imperfection/&quot;&gt;it&amp;#39;s been observed&lt;/a&gt; that
Bluesky&amp;#39;s timeline doesn&amp;#39;t serve much beyond the last day or two of content,
and that deeper cursor positions tend to fill with very recent posts rather
than true history. Treat the exact window as illustrative unless you&amp;#39;ve
measured it on your own deployment - the architectural point holds regardless
of the precise number.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;This leads to an interesting disparity: Data that is more than a few days old is
almost never accessed in the timeline. When it is accessed, it is usually
through a profile or a thread. Yet the reference implementation compiles every
timeline by joining those tables, which are terabyte-scale. This process is slow
and becomes slower as the tables grow, and timeline requests account for most of
what the DataPlane is asked to do.&lt;/p&gt;&lt;figure&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/inversion-7f5340cbaf0f4d018152cd5285cf78ca.png&quot; alt=&quot;Inversion of data volume and access frequency&quot; title=&quot;&quot;/&gt;&lt;figcaption&gt;Data volume and read frequency are inversely related: the vast bulk of data is old and almost never read, while the tiny sliver of recent posts drives nearly all timeline traffic.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;So timeline generation deserves a different treatment from record and thread
retrieval. The two workloads don&amp;#39;t just differ in degree. They have opposite
resource profiles, and they want different things from the runtime underneath.&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Hot path&lt;/strong&gt; - timelines&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Cold path&lt;/strong&gt; - records, threads, profiles&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Data age&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Recent (last day or two)&lt;/td&gt;&lt;td&gt;Historic (anything older)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Data volume&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;A tiny sliver&lt;/td&gt;&lt;td&gt;Terabytes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Request share&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;The dominant workload&lt;/td&gt;&lt;td&gt;Comparatively rare&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Bound by&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Memory + compute&lt;/td&gt;&lt;td&gt;I/O&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Lives in&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Memory&lt;/td&gt;&lt;td&gt;Database, fetched on demand&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Strategy&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Fan-out + bounded timeline length&lt;/td&gt;&lt;td&gt;Swappable datastore behind an interface&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;h2&gt;The hot path: timelines served from memory&lt;a href=&quot;https://bitcrowd.dev/why-elixir-bluesky-dataplane#the-hot-path-timelines-served-from-memory&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Most social platforms use a hybrid strategy to handle timeline fan-out. A post
from an account with a few thousand followers is pushed into its followers&amp;#39;
timelines as soon as it is published. This is known as &lt;strong&gt;fan-out on write&lt;/strong&gt; in
the lingo.&lt;/p&gt;&lt;p&gt;However, a post from an account with millions of followers (&lt;em&gt;&amp;quot;the celebrities&amp;quot;&lt;/em&gt;)
is not immediately pushed to all of its followers&amp;#39; mailboxes; instead, it is
merged in when a follower actually requests their timeline: This is called
&lt;strong&gt;fan-in on read&lt;/strong&gt;. The former keeps write amplification bounded for ordinary
accounts. The second prevents the write storm that a celebrity post would
otherwise cause.&lt;/p&gt;&lt;p&gt;Another obvious simplification becomes apparent when you stop thinking of a
social feed as an immutable archive. Take into account that users will jump
around with their attention and you will realise that timelines are finite.
Therefore, a user who follows ten thousand accounts will never see all of their
posts anyway. There&amp;#39;s reasonable to limit how much content is distributed to any
single timeline. The obligation is to provide a good, recent, bounded timeline,
not a complete one.&lt;/p&gt;&lt;p&gt;What this boils down to:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Recent posts - the overwhelming majority of what timelines are made of - can
live and be served &lt;strong&gt;from memory&lt;/strong&gt;.&lt;/li&gt;&lt;li&gt;Fan-out can be &lt;strong&gt;deferred&lt;/strong&gt;: as long as posts land in followers&amp;#39; timelines
within minutes, and the backlog of fan-out jobs doesn&amp;#39;t outgrow available
resources, nobody notices the delay.&lt;/li&gt;&lt;li&gt;Older content, when it&amp;#39;s genuinely needed, is safe to fetch from the database
on demand.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The dominant request type, the timeline read, stops being I/O-bound (wait on a
giant join) and becomes memory-and-compute-bound (manipulate in-memory
structures quickly). That shift changes what &amp;quot;fast&amp;quot; means, and it&amp;#39;s what
reopened the language question for us.&lt;/p&gt;&lt;p&gt;It also argues for putting the concrete database behind an interface. The hot
path barely touches it; the cold path is the only part that leans on it. Keep
that boundary clean and you can swap the backing store later without touching
the rest of the system.&lt;/p&gt;&lt;h3&gt;The follower graph: in memory, but not naively&lt;a href=&quot;https://bitcrowd.dev/why-elixir-bluesky-dataplane#the-follower-graph-in-memory-but-not-naively&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;There&amp;#39;s a catch. Fan-out and timeline assembly both need answers about the
follow graph: who follows whom, and the intersections and unions of those sets.
Holding hundreds of millions of follow relationships in memory as hash maps
would be ruinously wasteful.&lt;/p&gt;&lt;p&gt;Jaz&amp;#39;s &lt;a href=&quot;https://jazco.dev/2024/04/15/in-memory-graphs/&quot;&gt;&amp;quot;GraphD&amp;quot; series&lt;/a&gt;
documents this exact problem: an in-memory graph store that originally used hash
maps and hash sets to track each user&amp;#39;s followers and follows. The fix was
switching to &lt;a href=&quot;https://roaringbitmap.org/&quot;&gt;Roaring Bitmaps&lt;/a&gt;, a
&lt;a href=&quot;https://vikramoberoi.com/posts/a-primer-on-roaring-bitmaps-what-they-are-and-how-they-work/&quot;&gt;compressed bitmap structure&lt;/a&gt;
built for large set operations. The numbers are striking: the entire Bluesky
follow graph fits in roughly 6.5 GB of RAM, takes about 1.6 GB on disk, and
loads in around 20 seconds.&lt;/p&gt;&lt;p&gt;Jaz
&lt;a href=&quot;https://jazco.dev/2024/04/20/roaring-bitmaps/&quot;&gt;also describes two cost modes&lt;/a&gt;
that map onto our hot/cold split. Paging over &lt;em&gt;all&lt;/em&gt; of a user&amp;#39;s follows is
expensive and belongs in paginated or async fan-out jobs. On-demand set
intersection — &amp;quot;which people I follow also follow this person&amp;quot; — has to run at
interactive speed. Our split isn&amp;#39;t an invention; the access patterns already
worked this way in Bluesky&amp;#39;s own tooling.&lt;/p&gt;&lt;h2&gt;So what do we actually need from a language?&lt;a href=&quot;https://bitcrowd.dev/why-elixir-bluesky-dataplane#so-what-do-we-actually-need-from-a-language&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;With the workload pinned down, the requirements are:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Fast, compute-bound set operations over a large, long-lived, in-memory graph:
bitmap intersections and unions. Per-core compute, byte-level work, cache
sensitivity.&lt;/li&gt;&lt;li&gt;High-concurrency serving of memory-resident timeline reads, with bounded
response sizes and tight tail-latency expectations.&lt;/li&gt;&lt;li&gt;A deferrable fan-out queue that absorbs bursts, applies backpressure,
delivers within minutes, and degrades gracefully instead of falling over.&lt;/li&gt;&lt;li&gt;A clean datastore boundary for the cold path: records, threads, profiles.
Ordinary I/O-bound request handling.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The first three are the hot path; the fourth is the cold path. No language
obviously wins all four. This is the scorecard we judged each candidate against.&lt;/p&gt;&lt;h2&gt;The four candidates&lt;a href=&quot;https://bitcrowd.dev/why-elixir-bluesky-dataplane#the-four-candidates&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;At &lt;a href=&quot;https://bitcrowd.net/en&quot;&gt;bitcrowd&lt;/a&gt; we work with Elixir, Go and Rust day to
day, with the occasional Node project on the side. So this wasn&amp;#39;t a contest
between a favourite and a lineup of strangers - we had hands-on experience to
weigh on every side of the comparison.&lt;/p&gt;&lt;h3&gt;Go&lt;a href=&quot;https://bitcrowd.dev/why-elixir-bluesky-dataplane#go&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Go is the incumbent. It&amp;#39;s what Bluesky&amp;#39;s own production DataPlane is written in,
and the fit is strong. Goroutines and channels map cleanly onto &amp;quot;fan out work,
gather results, respond.&amp;quot; The bitmap work is comfortable in Go (GraphD itself is
Go). gRPC support is first-class, deployment is a single static binary, and the
tooling is mature. In-process fan-out is doable with worker pools draining
buffered channels.&lt;/p&gt;&lt;p&gt;The costs sit at the extremes. Under heavy allocation Go&amp;#39;s garbage collector
starts eating CPU, and at very high socket counts the network backend can
bottleneck on syscalls. Both respond to runtime tuning, but the tuning is
ongoing work, not a one-time fix. And the in-process fan-out you build yourself
comes with no supervision or isolation layer: a panicking worker takes the
process down, and backpressure and lifecycle management would be our&amp;#39;s to write.&lt;/p&gt;&lt;h3&gt;Rust&lt;a href=&quot;https://bitcrowd.dev/why-elixir-bluesky-dataplane#rust&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Rust gives the highest ceiling and the tightest control. No garbage collector
means no GC pauses in the tail latency. Tokio handles enormous concurrency with
low per-task overhead, tonic covers gRPC, and the roaring-bitmap and datastore
libraries are excellent. For the compute-bound half of our workload, nothing
beats it.&lt;/p&gt;&lt;p&gt;The cost is velocity. As this service is thin on business logic, we would pay
the price of the borrow checker and the sharp edges of async Rust on every line
while protecting very little. Fan-out with Tokio tasks and channels works very
well. However, as with Go, we would need to build lifecycle, backpressure and
supervision manually.&lt;/p&gt;&lt;h3&gt;Node / TypeScript&lt;a href=&quot;https://bitcrowd.dev/why-elixir-bluesky-dataplane#node--typescript&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Node deserves real consideration because it&amp;#39;s the language of the reference
DataPlane and the rest of the atproto stack: PDS, AppView frontend, lexicons.
One language across the codebase, shared types from lexicon definitions, the
largest hiring pool, the fastest iteration. For the cold path&amp;#39;s I/O-bound record
fetches it&amp;#39;s fine, and for modest-scale self-hosting it&amp;#39;s a sensible default.&lt;/p&gt;&lt;p&gt;The hot path is where it breaks down. One event loop per process means in-memory
queues and request serving compete for the same loop, and any CPU-bound work
blocks both. Using multiple cores means multiple processes with no shared
memory, which brings back the cross-process coordination the in-process design
was meant to remove, and makes a large shared in-memory graph awkward. It&amp;#39;s the
right tool for the reference implementation and a strained one for a
throughput-oriented production server.&lt;/p&gt;&lt;h3&gt;Elixir&lt;a href=&quot;https://bitcrowd.dev/why-elixir-bluesky-dataplane#elixir&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Elixir runs on the BEAM, a runtime built for massive numbers of cheap, isolated,
preemptively scheduled processes. Per-process garbage collection means no global
stop-the-world pauses, so tail latency stays flat under load. Supervision trees
give fault isolation and recovery essentially for free. For high-concurrency
request serving and a backpressured in-process queue, it&amp;#39;s the most naturally
suited runtime of the four.&lt;/p&gt;&lt;p&gt;It has one well-known weakness: raw per-core compute. The BEAM optimises for
concurrency and consistent latency, not single-threaded number-crunching.
Byte-level work like set operations over a large follower graph is exactly where
it&amp;#39;s slowest. Taken at face value, that rules it out for a workload with heavy
bitmap operations at its core.&lt;/p&gt;&lt;h2&gt;Every candidate has a flaw — which ones can you fix?&lt;a href=&quot;https://bitcrowd.dev/why-elixir-bluesky-dataplane#every-candidate-has-a-flaw--which-ones-can-you-fix&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Four reasonable options, each with one thing standing between it and a clean
fit. Instead of arguing it on paper, we prototyped toward each remedy, far
enough to tell whether the flaw could be engineered away or was structural.&lt;/p&gt;&lt;p&gt;Go&amp;#39;s GC and syscall overheads respond to tuning, and Bluesky&amp;#39;s production
DataPlane proves you can push Go a long way. But the tuning never ends. It&amp;#39;s a
knob you keep turning for the life of the service.&lt;/p&gt;&lt;p&gt;Node&amp;#39;s single event loop has exactly one fix, running more processes, and that
fix recreates the cross-process coordination and shared-state problems we were
designing out. There&amp;#39;s no way around it within the language.&lt;/p&gt;&lt;p&gt;Rust has no performance flaw. Its cost showed up as soon as we started building:
hand-rolled concurrency, lifecycle and backpressure machinery on every path, for
a service with little logic to protect. We realised that that cost would not
shrink over time, but stay with us with every change we would need to make.&lt;/p&gt;&lt;p&gt;Elixir&amp;#39;s flaw, per-core compute on tight loops, is real, and we hit it where
you&amp;#39;d expect: the follower-graph set operations. But it stayed in one place. It
didn&amp;#39;t smear across the service. It sat in a single, well-defined component we
could draw a boundary around, and a component with a sharp boundary can be
replaced. That&amp;#39;s what sent us back to Elixir for a second look.&lt;/p&gt;&lt;h2&gt;Resolving the Elixir paradox&lt;a href=&quot;https://bitcrowd.dev/why-elixir-bluesky-dataplane#resolving-the-elixir-paradox&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Can that component actually be lifted out? Yes, and the reason has less to do
with Elixir than with how our system is laid out.&lt;/p&gt;&lt;p&gt;Start with the claim itself, because it&amp;#39;s often stated too broadly. &amp;quot;The BEAM is
slow at CPU work&amp;quot; is true for tight numeric loops on a single core. It is not a
claim about how much hardware a real service needs. Most of what a production
server spends its time on isn&amp;#39;t a tight loop; it&amp;#39;s keeping thousands of
concurrent requests moving, and the BEAM is efficient at exactly that.&lt;/p&gt;&lt;p&gt;Per-process garbage collection means no global pauses, and tail latency stays
flat enough that you can run a node closer to its limit and still hit your
latency target.&lt;/p&gt;&lt;p&gt;This is why Elixir services often need fewer machines than the Go or Node
version of the same thing, occasionally approaching Rust, even though they lose
every microbenchmark. Hardware cost under concurrency and single-core throughput
are two different measurements, and only one of them shows up on your bill.&lt;/p&gt;&lt;p&gt;That said, our workload really does contain the thing the BEAM is bad at: the
roaring-bitmap intersections and unions over the follow graph. We don&amp;#39;t want to
talk our way around that, so we move it off the BEAM entirely.&lt;/p&gt;&lt;p&gt;The follower graph lives in a Rust implementation of Roaring Bitmaps, called
from Elixir as a NIF (Native Implemented Function). What makes this more than a
generic &amp;quot;wrap the slow part in Rust&amp;quot; patch is the shape of the data flow:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The input crossing the boundary is tiny: a user ID, or a small set of IDs.&lt;/li&gt;&lt;li&gt;The graph itself, millions of edges, stays on the Rust side in native memory.
The BEAM never holds it, never copies it, never garbage-collects it.&lt;/li&gt;&lt;li&gt;The expensive compute, the intersections and unions, runs entirely inside Rust
at native speed.&lt;/li&gt;&lt;li&gt;Only the result crosses back, and because timelines are length-limited, that
result is small by construction.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The usual objection to NIFs is the copy cost at the boundary. But in our case,
only small data crosses in both directions while the big structure and the heavy
computation stay native, which is the ideal case. That isn&amp;#39;t luck. The
timeline-length limit we&amp;#39;d already committed to is what bounds the return
values.&lt;/p&gt;&lt;p&gt;This subtracts the BEAM&amp;#39;s weakness from the hot path and keeps its strengths.
The compute-bound half runs in Rust regardless of host language. What&amp;#39;s left for
Elixir is orchestration: high-concurrency request serving and the fan-out queue,
the regime where the BEAM is at its best and where our own production experience
says it&amp;#39;s most efficient.&lt;/p&gt;&lt;p&gt;Of course, the NIF has its own price. It runs inside the BEAM&amp;#39;s memory space, so
a crash in the Rust code can take down the VM, and a long-running call can stall
a scheduler. So we give up some of the &amp;quot;let it crash&amp;quot; isolation exactly at the
native boundary. The mitigations fit this case well: the calls are short (small
in, bounded compute, small out), and the Rust surface is small, stable, and the
kind of code that rarely changes once written. Two languages is a real
maintenance cost, but &lt;a href=&quot;https://hexdocs.pm/rustler&quot;&gt;Rustler&lt;/a&gt; keeps the boundary
ergonomic, and a small, bounded Rust core seemed the cheapest of the available
evils.&lt;/p&gt;&lt;h2&gt;The deciding factor: fan-out as code, not infrastructure&lt;a href=&quot;https://bitcrowd.dev/why-elixir-bluesky-dataplane#the-deciding-factor-fan-out-as-code-not-infrastructure&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;The NIF made Elixir competitive. The fan-out requirement made it the choice.&lt;/p&gt;&lt;p&gt;Recall the spec: a deferrable queue that absorbs bursts, applies backpressure,
delivers within minutes, and degrades gracefully when the backlog grows. In Go,
Rust or Node, that almost always becomes an external system — Redis, NATS,
RabbitMQ, Kafka — because the language doesn&amp;#39;t give you primitives to do it
safely in-process.&lt;/p&gt;&lt;p&gt;On the BEAM, those primitives are the language. Processes are the workers,
mailboxes are the queues, supervisors handle recovery, and
&lt;a href=&quot;https://hexdocs.pm/gen_stage&quot;&gt;GenStage&lt;/a&gt; and
&lt;a href=&quot;https://hexdocs.pm/broadway&quot;&gt;Broadway&lt;/a&gt; add explicit backpressure, all inside
one VM with no network hop and nothing extra to deploy. Several costs disappear
outright:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;No serialisation across a queue boundary. An external queue serialises every
fan-out job on push and deserialises it on pop, putting per-event encoding
cost right back on the hot path we&amp;#39;d worked to keep it off. In-process, a job
is a message between processes.&lt;/li&gt;&lt;li&gt;No second system to operate. No separate scaling story, no &amp;quot;is Redis the
bottleneck now,&amp;quot; no disagreement between the service&amp;#39;s view of the backlog and
the queue&amp;#39;s.&lt;/li&gt;&lt;li&gt;One failure model. Supervision covers fan-out workers the same way it covers
everything else. There&amp;#39;s no seam between &amp;quot;the service crashed&amp;quot; and &amp;quot;the queue
is in a weird state.&amp;quot;&lt;/li&gt;&lt;li&gt;Backpressure in-band. Producer and consumer share a runtime, so the producer
notices the consumer falling behind directly instead of inferring it from
queue-depth metrics after the fact.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The trade is durability. In-process means in-memory, and an in-memory backlog
dies with the node. For us that&amp;#39;s acceptable: fan-out is best-effort timeline
population, timelines are bounded and ageing, and the fan-in path on read covers
whatever goes missing for a window. We&amp;#39;re choosing fast, simple and
lossy-on-crash over durable, external and heavier, and the choice only works
because the rest of the architecture makes the loss cheap. If you need
durability here, much of the in-process advantage narrows. Check this assumption
against your own tolerances.&lt;/p&gt;&lt;h2&gt;Why Elixir&lt;a href=&quot;https://bitcrowd.dev/why-elixir-bluesky-dataplane#why-elixir&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;We love working with Elixir, but we did not assume it to be the tool of choice
for this project. The ecosystem just did not seem to be waiting for it. Choosing
it means making the case for it, repeatedly, starting with this post.&lt;/p&gt;&lt;p&gt;And Elixir doesn&amp;#39;t win every category. We came to the decision to use it
because, for this specific workload, the one axis it loses on — per-core compute
for graph operations — is the one we can cleanly offload to a small Rust NIF,
with a data flow that makes the offload nearly free.&lt;/p&gt;&lt;p&gt;Everything that remains is concurrency, coordination, predictable tail latency
under load, and a burst-absorbing fan-out queue we can build in the program
itself instead of bolting on as infrastructure. Go would have been the
pragmatic, proven middle.&lt;/p&gt;&lt;p&gt;Rust would have given us the highest ceiling at the cost of velocity and a lot
of hand-rolled concurrency machinery. Node was right for the reference
implementation and wrong for a memory-bound production server. Elixir plus a
thin Rust core covers both halves of a workload that genuinely has two halves.&lt;/p&gt;&lt;h2&gt;What next?&lt;a href=&quot;https://bitcrowd.dev/why-elixir-bluesky-dataplane#what-next&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Most of this is architecture-level reasoning informed by the workload&amp;#39;s shape
and each runtime&amp;#39;s known characteristics. We backed this up with head-to-head
benchmarks between the Node reference implementation and our Elixir POC.&lt;/p&gt;&lt;p&gt;For that, we needed to build a performance evaluation toolkit for our studies.
If you want to find out the limits of your setup,
&lt;a href=&quot;https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto&quot;&gt;check it out&lt;/a&gt;&lt;/p&gt;&lt;p&gt;The pieces we lean on hardest are well-sourced: Jaz&amp;#39;s GraphD work for the
in-memory graph and Roaring Bitmaps, and the documented existence of a
fan-in/fan-out split between expensive paging and interactive-speed set
intersection. The pieces we&amp;#39;re least certain about - the exact timeline window,
the precise per-request CPU breakdown - we&amp;#39;ve flagged as such.&lt;/p&gt;&lt;p&gt;We have build and tested a
&lt;a href=&quot;https://bitcrowd.dev/timelines-from-elixir&quot;&gt;proof of concept&lt;/a&gt; that
serves the skeleton amazingly fast. What is missing now is the boring part of
writing the &amp;gt; 90 endpoints of the DataPlane API and the indexer plugins.
Currently, it&amp;#39;s a side project.
&lt;a href=&quot;https://cal.eu/bitcrowd/discuss-a-project&quot;&gt;Give us a shout&lt;/a&gt; if you want to help
us take it further, faster.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Your Orchestrator Is a Finite Automaton in Denial</title>
<link>https://rocket-science.ru/hacking/2026/06/14/status-driven-orchestrator-in-denial</link>
<enclosure type="image/jpeg" length="0" url="https://rocket-science.ru/img/logo/logo-orig.png"></enclosure>
<guid isPermaLink="false">5ghBBH0F5npiisbndTG8m73jcj-6rvTi17EXwg==</guid>
<pubDate>Tue, 16 Jun 2026 03:03:39 +0000</pubDate>
<description>On the lonely `status` column that quietly metastasised into a state machine, the boolean flags breeding in its shadow, and why a real finite automaton is not academic finery but the cheapest insurance you will ever decline to buy</description>
<content:encoded>&lt;p&gt;Somewhere in your codebase there is a table with a column called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status&lt;/code&gt;. It started life as the most innocent thing imaginable—a single string, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;quot;pending&amp;quot;&lt;/code&gt;, set once at insertion and forgotten. Then somebody needed to know whether the thing had been paid for, so a boolean joined the schema. Then somebody needed to know whether it had shipped, and another boolean arrived. By the time the quarter closed, your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status&lt;/code&gt; column had accreted flags the way a ship’s hull accretes barnacles: quietly, asymmetrically, and always below the waterline where nobody looks until the thing stops steering.&lt;/p&gt;&lt;p&gt;This is &lt;em&gt;status-driven orchestration&lt;/em&gt;, and it is the most popular way in the industry to build a finite-state machine while loudly insisting you are doing nothing of the sort. The denial is the interesting part. Everyone agrees that explicit state machines are good in the abstract—the way everyone agrees that flossing is good—and then goes home and writes another &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cond/1&lt;/code&gt; that branches on a string.&lt;/p&gt;&lt;p&gt;Let me try to talk you out of it.&lt;/p&gt;&lt;h2&gt;The Anatomy of the Thing You Built&lt;/h2&gt;&lt;p&gt;Here is a perfectly representative specimen. An order goes through a fulfilment pipeline: it gets paid for, packed, shipped, delivered. It can be cancelled. It can be refunded. Nothing exotic. Let us model it the way it actually gets modelled in the wild, in an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ecto&lt;/code&gt; schema that has clearly survived three product managers:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;schema &amp;quot;orders&amp;quot; do
  field :status,     :string,  default: &amp;quot;pending&amp;quot;
  field :paid?,      :boolean, default: false
  field :packed?,    :boolean, default: false
  field :shipped?,   :boolean, default: false
  field :delivered?, :boolean, default: false
  field :cancelled?, :boolean, default: false
  field :refunded?,  :boolean, default: false
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;And here is the orchestrator that drives it—the beating heart of the system, the thing that gets paged at three in the morning:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;def advance(%Order{} = order) do
  cond do
    order.status == &amp;quot;pending&amp;quot; and order.paid? and not order.cancelled? -&amp;gt;
      update(order, status: &amp;quot;paid&amp;quot;)

    order.status == &amp;quot;paid&amp;quot; and order.packed? and not order.refunded? -&amp;gt;
      update(order, status: &amp;quot;packed&amp;quot;)

    order.status == &amp;quot;packed&amp;quot; and order.shipped? -&amp;gt;
      update(order, status: &amp;quot;shipped&amp;quot;)

    order.status == &amp;quot;shipped&amp;quot; and order.delivered? -&amp;gt;
      update(order, status: &amp;quot;delivered&amp;quot;)

    order.cancelled? -&amp;gt;
      update(order, status: &amp;quot;cancelled&amp;quot;)

    true -&amp;gt;
      {:ok, order}
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;It looks reasonable. It compiles. It passes the one test somebody wrote for the happy path. And it is, I regret to inform you, a catastrophe wearing the high-visibility vest of a solution.&lt;/p&gt;&lt;h2&gt;What Is Actually Wrong Here&lt;/h2&gt;&lt;h3&gt;Illegal states are not merely possible, they are the majority&lt;/h3&gt;&lt;p&gt;Count the representable states. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status&lt;/code&gt; string takes seven values; each of the six booleans doubles the space. That is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;7 × 2⁶ = 448&lt;/code&gt; distinct rows your database will cheerfully accept. Of those four hundred and forty-eight, perhaps seven correspond to an order that could exist in physical reality.&lt;/p&gt;&lt;p&gt;The other four hundred and forty-one are nonsense, and your schema treats them with exactly the same hospitality as the legal ones. Nothing—not a constraint, not a type, not a stern comment—prevents a row with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status: &amp;quot;delivered&amp;quot;&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cancelled?: true&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;refunded?: true&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;paid?: false&lt;/code&gt;. That is an order that was never paid for, was cancelled, was refunded money it never received, and was nonetheless delivered. It sits in your database like a passenger holding a ticket for a train that was cancelled: still on the platform, still expecting to be taken somewhere, and now also somehow already at the destination.&lt;/p&gt;&lt;p&gt;The flags breed in the schema like adapters in a junk drawer—each one solved a real problem exactly once, and now you own six and can confidently explain three. Every new boolean does not &lt;em&gt;add&lt;/em&gt; a state; it &lt;em&gt;multiplies&lt;/em&gt; the space of states you have promised to reason about and silently declined to.&lt;/p&gt;&lt;h3&gt;The state machine exists; you simply refused to draw it&lt;/h3&gt;&lt;p&gt;There is a finite-state machine in that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;advance/1&lt;/code&gt; function. It is real, it has transitions, it has rules about what may follow what. The only problem is that it has no single, inspectable existence. It is smeared across the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cond&lt;/code&gt;, the changeset validations, three controller actions, a background job, and the part of the senior engineer’s memory that he is planning to take with him when he leaves. The transition table lives nowhere and everywhere at once, like a signature forged by committee.&lt;/p&gt;&lt;p&gt;Ask the system a simple question—&lt;em&gt;what states can follow &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;paid&lt;/code&gt;?&lt;/em&gt;—and there is no honest way to answer it except to read the entire codebase and hope you found every site that writes to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status&lt;/code&gt;. Spoiler: you did not. There is one in a Rake-equivalent task from 2023 that sets &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status = &amp;quot;shipped&amp;quot;&lt;/code&gt; directly, bypassing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;advance/1&lt;/code&gt; entirely, because someone needed to fix a stuck order once and never removed the scaffolding.&lt;/p&gt;&lt;h3&gt;Nothing stops a transition that should be unthinkable&lt;/h3&gt;&lt;p&gt;Because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status&lt;/code&gt; is just a field, &lt;em&gt;any&lt;/em&gt; code that can reach the struct can write &lt;em&gt;any&lt;/em&gt; value to it. There is no notion of a transition being illegal—there is only the notion of you having remembered to write an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if&lt;/code&gt; that forbids it, everywhere, forever, without exception. Going from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delivered&lt;/code&gt; back to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pending&lt;/code&gt; is not prevented by the design; it is prevented by your vigilance, which is a renewable resource right up until the on-call rotation hits someone new.&lt;/p&gt;&lt;h3&gt;Concurrency turns the whole thing into a knife fight in a lift&lt;/h3&gt;&lt;p&gt;Two requests arrive at once. Both read the order in state &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pending&lt;/code&gt;. Both evaluate the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cond&lt;/code&gt;. Both decide the next state. Both write. What follows is less a race condition than a knife fight in a lift: cramped, badly lit, and with exactly one party walking out. Last writer wins, the first update evaporates, and the customer is charged twice because the “already paid?” check read a value that was true a microsecond ago and is now a lie.&lt;/p&gt;&lt;p&gt;You can paper over this with row locks and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SELECT ... FOR UPDATE&lt;/code&gt; and optimistic-concurrency version columns, and now you are hand-rolling the serialisation guarantees that a process-based state machine hands you for nothing.&lt;/p&gt;&lt;h3&gt;Failure is an afterthought wearing a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rescue&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;Where, in the specimen above, does failure live? It does not. When the payment processor times out, the code raises, something up the stack catches it, sets &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status: &amp;quot;failed&amp;quot;&lt;/code&gt;—an eighth string value nobody added to the schema’s mental model—and the order joins the population of rows that no longer match any branch of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cond&lt;/code&gt; and will sit there, inert, until a human notices the revenue gap.&lt;/p&gt;&lt;h3&gt;You overwrote your own audit trail&lt;/h3&gt;&lt;p&gt;Every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update(order, status: &amp;quot;paid&amp;quot;)&lt;/code&gt; destroys the evidence of where the order was a moment ago. The history of the process—the single most valuable thing you have when debugging why an order is stuck—is overwritten in place. Reconstructing it later is archaeology conducted with a teaspoon and a head-torch, cross-referencing log lines against &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;updated_at&lt;/code&gt; timestamps and praying nobody ran a backfill.&lt;/p&gt;&lt;h2&gt;Now Do It With an Actual Finite Automaton&lt;/h2&gt;&lt;p&gt;Here is the same pipeline as a real FSM, using my &lt;a href=&quot;https://hexdocs.pm/finitomata&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Finitomata&lt;/code&gt;&lt;/a&gt; library. The entire state machine—every state, every legal transition, every event that triggers it—is declared once, in plain text, in a format that is simultaneously the code, the documentation, and a diagram your product manager can read:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;defmodule Order.FSM do
  @fsm &amp;quot;&amp;quot;&amp;quot;
  pending --&amp;gt; |pay| paid
  pending --&amp;gt; |cancel| cancelled
  pending --&amp;gt; |expire| cancelled
  paid --&amp;gt; |pack| packed
  paid --&amp;gt; |refund?| refunded
  packed --&amp;gt; |ship| shipped
  shipped --&amp;gt; |deliver| delivered
  &amp;quot;&amp;quot;&amp;quot;

  use Finitomata, fsm: @fsm, auto_terminate: true, timer: 15 * 60_000
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;That is not pseudocode and it is not a comment. That &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@fsm&lt;/code&gt; string is parsed, validated, and compiled into a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GenServer&lt;/code&gt; with all the transition machinery generated for you. The diagram &lt;em&gt;is&lt;/em&gt; the source of truth, because there is no other source for it to disagree with.&lt;/p&gt;&lt;p&gt;Then you implement the business logic, and &lt;em&gt;only&lt;/em&gt; the business logic, in callbacks that pattern-match on exactly the state-and-event pairs you care about:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;@impl Finitomata
def on_transition(:pending, :pay, %{amount: amount}, payload) do
  case Payments.charge(payload.customer, amount) do
    {:ok, receipt} -&amp;gt; {:ok, :paid, Map.put(payload, :receipt, receipt)}
    {:error, _reason} -&amp;gt; {:error, :payment_declined}
  end
end

def on_transition(:paid, :pack, _event_payload, payload),
  do: {:ok, :packed, payload}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;A successful charge moves the machine to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:paid&lt;/code&gt; and stashes the receipt in the payload. A declined charge returns an error, the machine &lt;em&gt;stays exactly where it was&lt;/em&gt;, and control flows to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;on_failure/3&lt;/code&gt;—a first-class, named place for things going wrong, rather than a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rescue&lt;/code&gt; clause hoping for the best:&lt;/p&gt;&lt;p&gt;Time itself becomes a transition rather than a shadow orchestrator. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;timer: 15 * 60_000&lt;/code&gt; option calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;on_timer/2&lt;/code&gt; on a schedule, so an unpaid order expires on its own, without a cron job somewhere running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WHERE status = &amp;#39;pending&amp;#39; AND inserted_at &amp;lt; ...&lt;/code&gt; and mutating rows behind the FSM’s back:&lt;/p&gt;&lt;p&gt;And driving it from the outside is unceremonious:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;{:ok, _pid} = Finitomata.start_link()

Finitomata.start_fsm(Order.FSM, &amp;quot;order:42&amp;quot;, %{customer: cust, amount: 9_900})
Finitomata.transition(&amp;quot;order:42&amp;quot;, {:pay, %{amount: 9_900}})

Finitomata.state(&amp;quot;order:42&amp;quot;)
#⇒ %Finitomata.State{current: :paid, history: [:pending], payload: %{…}}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Notice the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;history&lt;/code&gt; field. The machine remembers where it has been, for free, without you overwriting anything.&lt;/p&gt;&lt;h2&gt;Point by Point, Why This One Wins&lt;/h2&gt;&lt;h3&gt;Illegal states stop being representable&lt;/h3&gt;&lt;p&gt;The current state is a single atom drawn from a closed set the compiler knows about. There is no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status&lt;/code&gt; string &lt;em&gt;and&lt;/em&gt; a constellation of booleans to fall out of sync; there is the state, and there is the payload, and they are different things with different jobs. The four hundred and forty-one nonsense rows simply have nowhere to live. You cannot be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delivered&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cancelled&lt;/code&gt; simultaneously for the same brutally simple reason you cannot be in two rooms at once: the model does not have a word for it.&lt;/p&gt;&lt;h3&gt;The transition table is validated before your code ever runs&lt;/h3&gt;&lt;p&gt;This is the part that ought to close the argument by itself. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:finitomata&lt;/code&gt; compiler reads your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@fsm&lt;/code&gt; declaration and refuses to proceed unless it is a &lt;em&gt;consistent&lt;/em&gt; machine: exactly one initial state, at least one final state, and no orphans—no state you can enter and never leave, no state you declared and can never reach. It refuses to compile an incoherent machine the way a good editor refuses a sentence that parses but lies.&lt;/p&gt;&lt;p&gt;Better still, if you add a transition to the diagram and forget to handle an ambiguous case in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;on_transition/4&lt;/code&gt;, the compiler &lt;em&gt;tells you&lt;/em&gt;, at compile time, with a warning that names the gap. Compare this to the status-driven approach, where a forgotten branch is discovered by a customer, on a Saturday, via Twitter.&lt;/p&gt;&lt;h3&gt;Transitions are guarded by construction, not by your memory&lt;/h3&gt;&lt;p&gt;A transition that is not in the diagram does not happen. Sending &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:deliver, …}&lt;/code&gt; to an order in state &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pending&lt;/code&gt; is not a bug you must remember to prevent with an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if&lt;/code&gt;; it is structurally impossible, ignored by the machine the way a vending machine ignores a button for a slot that does not exist. The set of legal moves is data, declared once, enforced everywhere, rather than folklore re-implemented at each call site.&lt;/p&gt;&lt;h3&gt;Concurrency is solved because each machine is a process&lt;/h3&gt;&lt;p&gt;Every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Finitomata&lt;/code&gt; instance is its own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GenServer&lt;/code&gt;, which means transition requests for a single order are serialised through a single mailbox and processed one at a time, in order. The knife-fight-in-a-lift disappears, not because you were careful, but because the architecture made the fight impossible to start. No row locks, no version columns, no optimistic-concurrency retries—just the actor model doing the one thing it has always been excellent at.&lt;/p&gt;&lt;h3&gt;Failure, timeouts, and retries are vocabulary, not accidents&lt;/h3&gt;&lt;p&gt;The library has named, first-class places for the things status-driven code handles by flailing: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;on_failure/3&lt;/code&gt; for transitions that did not complete, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;on_timer/2&lt;/code&gt; for the passage of time, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ensure_entry:&lt;/code&gt; option to retry a transition until it sticks, and the last error preserved in the state for when you need to know &lt;em&gt;why&lt;/em&gt;. A transition that ends with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;?&lt;/code&gt;—like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;paid --&amp;gt; |refund?| refunded&lt;/code&gt;—is declared as one that is &lt;em&gt;expected&lt;/em&gt; to sometimes fail, so it does so quietly, without crying wolf in your logs. A transition that ends with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;!&lt;/code&gt; is &lt;em&gt;determined&lt;/em&gt; and fires the instant it becomes the only way forward. These are not features bolted on; they are the grammar of the thing.&lt;/p&gt;&lt;h3&gt;History and observability come included&lt;/h3&gt;&lt;p&gt;The state carries its own history. When an order is stuck, you ask the machine where it has been and it tells you, instead of you reconstructing the past from the sediment of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;updated_at&lt;/code&gt;. Pair it with the &lt;a href=&quot;https://hexdocs.pm/finitomata&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;telemetria&lt;/code&gt;&lt;/a&gt; integration and every transition is an event you can measure, rather than a mutation you have to infer.&lt;/p&gt;&lt;h3&gt;Testing stops being string-equality theatre&lt;/h3&gt;&lt;p&gt;Because the machine is explicit, you can test the machine. &lt;a href=&quot;https://hexdocs.pm/finitomata/Finitomata.ExUnit.html&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Finitomata.ExUnit&lt;/code&gt;&lt;/a&gt; lets you walk a path through the states and assert on each transition and the resulting payload, with a syntax that reads like the thing it verifies:&lt;/p&gt;&lt;p&gt;The status-driven equivalent is mocking the database and asserting that a string equals another string, which tests your typing accuracy and very little else.&lt;/p&gt;&lt;h3&gt;Distribution is a one-line upgrade&lt;/h3&gt;&lt;p&gt;When one node is no longer enough, swap &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Finitomata&lt;/code&gt; for &lt;a href=&quot;https://hexdocs.pm/finitomata/Infinitomata.html&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Infinitomata&lt;/code&gt;&lt;/a&gt; and your machines run transparently across the cluster on top of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:pg&lt;/code&gt;, with no change to the business logic. Scaling becomes a matter of adding nodes, not of rewriting your orchestrator around a distributed lock you will get subtly wrong.&lt;/p&gt;&lt;h2&gt;The Objections, and Why They Fold&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;“This is over-engineering. It’s just a status field.”&lt;/strong&gt; It is not just a status field, and the proof is the six booleans standing next to it. You did not avoid building a state machine; you built one and declined to name it, which is the single option strictly worse than both alternatives—you pay the full cost of the complexity and receive none of the guarantees. Naming the tumour does not create it. It lets you operate.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;“FSMs are academic, textbook stuff.”&lt;/strong&gt; So is binary search, which you use without flinching, and the hash table underneath every dictionary you have ever instantiated. “Academic” is what we call the ideas that turned out to be so correct they became invisible. The finite automaton is one of the oldest, most thoroughly understood objects in computer science. Refusing to use it because it has a formal name is like refusing to use a bridge because an engineer was involved.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;“We already use a workflow engine for this.”&lt;/strong&gt; A workflow engine is, in the cases that matter, a finite-state machine that hired a sales team and learned to bill by the seat. If you want the semantics, you can have them in a hundred and fifty lines of your own language, in your own repository, without the YAML, the vendor console, and the per-execution pricing. Sometimes the heavy engine is the right call. Usually it is a sledgehammer rented monthly to drive a thumbtack.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;“Our orchestrator works fine.”&lt;/strong&gt; So does a car with insulating tape over the check-engine light. “Works fine” is a statement about the inputs you have happened to receive so far, not about the four hundred and forty-one illegal states patiently waiting for the input that produces them.&lt;/p&gt;&lt;h2&gt;The Point&lt;/h2&gt;&lt;p&gt;You are going to build a state machine. That decision was made the moment your process had more than one step. The only choice left to you is whether it will be an &lt;em&gt;explicit&lt;/em&gt; state machine—declared in one place, validated by the compiler, guarded by construction, serialised by the runtime, and testable as a unit—or an &lt;em&gt;implicit&lt;/em&gt; one, scattered across your codebase like cutlery after an earthquake, held together by a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status&lt;/code&gt; string, a fistful of booleans, and the fervent hope that nobody writes to the field from somewhere you forgot about.&lt;/p&gt;&lt;p&gt;The implicit machine is not simpler. It is the same machine with the guarantees filed off and the documentation set on fire. Name it. Draw it. Let the compiler check it. Your three-in-the-morning self, squinting at a row that claims to be delivered and refunded and never paid for, will thank you with a sincerity your waking self is not capable of.&lt;/p&gt;&lt;p&gt;Happy—and finite—automating.&lt;/p&gt;&lt;hr/&gt;&lt;p&gt;Previously, on the subject of refusing to lose your mind over state:&lt;/p&gt;&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://rocket-science.ru/hacking/2024/03/03/finitomata-for-the-win&quot;&gt;Finitomata FTW&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://rocket-science.ru/hacking/2024/05/24/finitomata-exunit&quot;&gt;Make Your Library Test-Friendly&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://hexdocs.pm/finitomata&quot;&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded>
</item>
<item>
<title>Serving timelines from Elixir</title>
<link>https://bitcrowd.dev/timelines-from-elixir</link>
<guid isPermaLink="false">olUyjOHUSPkBNpoVkLx3lMKVxZn6x4b-wcqAdw==</guid>
<pubDate>Tue, 02 Jun 2026 04:24:30 +0000</pubDate>
<description>Recently, we presented our performance toolkit for atproto including a small Proof-of-Concept for a dataplane written in Elixir.</description>
<content:encoded>&lt;p&gt;Recently, we presented our &lt;a href=&quot;https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto&quot;&gt;performance toolkit for atproto&lt;/a&gt; including a small Proof-of-Concept for a dataplane written in Elixir.
We didn&amp;#39;t go too much into the details though.
We will make up for that in this post.&lt;/p&gt;&lt;p&gt;Let&amp;#39;s start by recapping the problem.&lt;/p&gt;&lt;p&gt;The &lt;a href=&quot;https://atproto.com/&quot;&gt;AT Protocol&lt;/a&gt; enables us to build an open social ecosystem of interoperable applications.
By far the most popular application in this ecosystem today is &lt;a href=&quot;https://bsky.app/&quot;&gt;Bluesky&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Bluesky has open sourced almost all of their software.
You can find their &lt;a href=&quot;https://github.com/bluesky-social/social-app&quot;&gt;app&lt;/a&gt;, their &lt;a href=&quot;https://github.com/bluesky-social/indigo&quot;&gt;Go services&lt;/a&gt; and their &lt;a href=&quot;https://github.com/bluesky-social/atproto&quot;&gt;TypeScript code&lt;/a&gt; (backend services and protocol reference implementation).&lt;/p&gt;&lt;p&gt;This is their production code, with one exception: they don&amp;#39;t run the open source dataplane.
They&amp;#39;ve replaced it with a closed source implementation that is more performant.&lt;/p&gt;&lt;figure&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/bluesky-5fcedfef78dd330f8c008f30b0457a02.png&quot; alt=&quot;Bluesky architecture overview&quot; title=&quot;&quot;/&gt;&lt;figcaption&gt;The dataplane is a central component of the AppView in Bluesky&amp;#39;s architecture. Read the&lt;a href=&quot;https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto&quot;&gt;previous post&lt;/a&gt;to learn more.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;We talked about some of the issues of the open source dataplane in our &lt;a href=&quot;https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#the-problems-of-the-dataplane&quot;&gt;previous blog post&lt;/a&gt;.&lt;br/&gt;&lt;strong&gt;The bottom line is&lt;/strong&gt;: it won&amp;#39;t scale if you&amp;#39;re trying to serve hundreds of thousands of users.&lt;/p&gt;&lt;h2&gt;Why you should care&lt;a href=&quot;https://bitcrowd.dev/timelines-from-elixir#why-you-should-care&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Even the Bluesky people think that &lt;a href=&quot;https://atproto.com/blog/protocol-check-in-fall-2025#hard-decentralization&quot;&gt;hard decentralization&lt;/a&gt; is necessary.
What is the point of an open ecosystem when everything is tied to the existence of a single company?&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Bluesky can be bought and start to enshittify&lt;/li&gt;&lt;li&gt;Bluesky operates under US law, why should we want our online social life to be ruled by mad people inside a white house&lt;/li&gt;&lt;li&gt;Bluesky has &lt;a href=&quot;https://pckt.blog/b/jcalabro/april-2026-outage-post-mortem-219ebg2&quot;&gt;outages&lt;/a&gt;, alternatives could continue to operate&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So, we should want alternative providers of Bluesky to exist.&lt;/p&gt;&lt;p&gt;Luckily for us, there is already a number of organizations picking up that work.&lt;br/&gt;Luckily for them, they can build on the open source components that Bluesky provides.&lt;br/&gt;Unluckily for them, they are left with a dataplane implementation that won&amp;#39;t work for them when their userbase grows.&lt;/p&gt;&lt;p&gt;As a result, the Blacksky people have to spend their time rewriting &lt;a href=&quot;https://github.com/blacksky-algorithms/rsky/tree/main/rsky-wintermute&quot;&gt;components in Rust&lt;/a&gt; and our friends from Eurosky started exploring &lt;a href=&quot;https://github.com/yawn/crimeline&quot;&gt;alternative ways to build timelines&lt;/a&gt;.&lt;/p&gt;&lt;h2&gt;Serving timelines from Elixir&lt;a href=&quot;https://bitcrowd.dev/timelines-from-elixir#serving-timelines-from-elixir&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Elixir provides a number of benefits for a backend service like the dataplane.&lt;br/&gt;You work on a high level of abstraction, getting more done in the same amount of time and end up with very readable code (yes, this is still important, even for your agent!).&lt;br/&gt;At the same time you get parallelism for free and performance that scales with the cores of the system.&lt;br/&gt;Errors are isolated, so they don&amp;#39;t bring down the whole service.
As a bonus, the VM provides you with good tooling for introspection and observability, all out of the box.&lt;/p&gt;&lt;p&gt;So, when we built our &lt;a href=&quot;https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#a-performant-open-source-dataplane&quot;&gt;Proof-of-Concept&lt;/a&gt;, we naturally chose Elixir.
We wanted to investigate the performance issues that will come up in the open source dataplane implementation.
For this reason, we focused on implementing the critical endpoint which is responsible for serving timelines to users.&lt;/p&gt;&lt;p&gt;From our exploration in the last blog post, we already know that the most important decision to solve the issues of Bluesky&amp;#39;s open source database implementation is to change the approach from fan-in to fan-out.&lt;/p&gt;&lt;p&gt;When a user requests their timeline from Bluesky&amp;#39;s open source database implementation it will query the posts that should be in the timeline on demand.
This is known as fan-in principle and is a major reason for the performance issues of this implementation.&lt;/p&gt;&lt;p&gt;Consequently, the implementation used by Bluesky in production turns this process on the head, known as the fan-out principle.
It constructs the timeline for each user whenever a new post arrives in the system.
When the user requests their timeline, the data is already waiting for them to be served.&lt;/p&gt;&lt;h3&gt;ETS tables&lt;a href=&quot;https://bitcrowd.dev/timelines-from-elixir#ets-tables&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;With this insight, it was clear to us that we should build our dataplane on the fan-out principle.
It was less clear how we should store the data we need.
The Erlang VM has multiple built-in options for storing data, so choosing one of them, the &lt;a href=&quot;https://www.erlang.org/doc/apps/stdlib/ets.html&quot;&gt;Erlang Term Storage&lt;/a&gt; (ETS), was a good starting point.&lt;/p&gt;&lt;p&gt;Citing the documentation:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Data is organized as a set of dynamic tables, which can store tuples.&lt;br/&gt;...&lt;br/&gt;Tables are divided into four different types, set, ordered_set, bag, and duplicate_bag. A set or ordered_set table can only have one object associated with each key. A bag or duplicate_bag table can have many objects associated with each key.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;ETS stores data as tuples in tables in memory but which tables do we need to store all the data we need to serve timelines?
And of which type should the tables be?&lt;/p&gt;&lt;p&gt;To store follows in a way that we can retrieve all the information we need efficiently, we actually need two tables: &lt;code&gt;followers_table&lt;/code&gt; and &lt;code&gt;following_table&lt;/code&gt;.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;:ets.new(@followers_table,[
:duplicate_bag,
:named_table,
:public,
read_concurrency:true,
write_concurrency:true
])

:ets.new(@following_table,[
:duplicate_bag,
:named_table,
:public,
read_concurrency:true,
write_concurrency:true
])&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;These store the follows information in both directions, holding pairs of user IDs.&lt;br/&gt;We can find all the followers of a user by looking up the entries with their user ID as key in the &lt;code&gt;followers_table&lt;/code&gt;.
And we can find all the users this user follows by looking up the entries with their user ID as key in the &lt;code&gt;following_table&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Both tables are of type &lt;code&gt;:duplicate_bag&lt;/code&gt;.&lt;br/&gt;We need multiple entries per key, so we must use either &lt;code&gt;:bag&lt;/code&gt; or &lt;code&gt;:duplicate_bag&lt;/code&gt; as type of the table.
&lt;code&gt;:bag&lt;/code&gt; would have the benefit of preventing duplicates, however we chose &lt;code&gt;:duplicate_bag&lt;/code&gt; for its performance benefits.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;posts_table&lt;/code&gt; represents the source of truth for all the posts in the system.
It&amp;#39;s a &lt;code&gt;:set&lt;/code&gt; storing post IDs as we don&amp;#39;t want duplicates here.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;:ets.new(@posts_table,[
:set,
:named_table,
:public,
read_concurrency:true,
write_concurrency:true,
decentralized_counters:true
])&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;In the &lt;code&gt;feeds_table&lt;/code&gt; we store the feeds of all users.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;:ets.new(@feeds_table,[
:duplicate_bag,
:named_table,
:public,
read_concurrency:true,
write_concurrency:true,
decentralized_counters:true
])&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Whenever a new post arrives in our system, we look up all the followers of the author of the post using the &lt;code&gt;followers_table&lt;/code&gt;.
Then, for each follower we store a pair of the follower&amp;#39;s user ID and the post ID in the &lt;code&gt;feeds_table&lt;/code&gt;.
We chose the type &lt;code&gt;:duplicate_bag&lt;/code&gt; here for the same reasons as we did for the &lt;code&gt;followers_table&lt;/code&gt;.
We need multiple entries per key (user ID) and we want the performance benefits of &lt;code&gt;:duplicate_bag&lt;/code&gt; over &lt;code&gt;:bag&lt;/code&gt;.&lt;/p&gt;&lt;h4&gt;Celebrities&lt;a href=&quot;https://bitcrowd.dev/timelines-from-elixir#celebrities&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;With these tables we would have a functioning system.
However, there is still the &lt;code&gt;celebrity_posts_table&lt;/code&gt; to talk about.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;:ets.new(@celebrity_posts_table,[
:duplicate_bag,
:named_table,
:public,
read_concurrency:true,
write_concurrency:true,
decentralized_counters:true
])&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This table is actually a first optimization of the system.
You might have wondered what would happen if a post arrives in the system authored by a user who has many many followers.
If we would stick to our &lt;code&gt;feeds_table&lt;/code&gt;, we would have to loop through all of them to insert the corresponding data.&lt;/p&gt;&lt;p&gt;For this reason, we introduced the &lt;code&gt;celebrity_posts_table&lt;/code&gt;.
The key idea here is that if a user has too many followers (is a celebrity), we give their posts a special treatment to avoid looping through all the followers and the associated performance issues.
Instead we will store pairs of the author and post IDs in &lt;code&gt;celebrity_posts_table&lt;/code&gt;, which is again of type &lt;code&gt;:duplicate_bag&lt;/code&gt;.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;definsert_post(post_id, author_id)do
:ets.insert(@posts_table,{post_id, author_id})

  follower_entries =:ets.lookup(@followers_table, author_id)
  limit =fan_out_limit()

if limit ==:infinityorlength(follower_entries)&amp;lt;= limit do
Enum.each(follower_entries,fn{_subject, follower_id}-&amp;gt;
:ets.insert(@feeds_table,{follower_id, post_id})
end)
else
:ets.insert(@celebrity_posts_table,{author_id, post_id})
end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;When a user request their timeline, we will first look for their feed in the &lt;code&gt;feeds_table&lt;/code&gt;, then do another lookup in the &lt;code&gt;celebrity_posts_table&lt;/code&gt; to gather posts from celebrities they follow.
The user will then see a combination of those posts.
So, in fact this dataplane combines fan-out (for regular posts in &lt;code&gt;feeds_table&lt;/code&gt;) with fan-in (for celebrity posts in &lt;code&gt;celebrity_posts_table&lt;/code&gt;) for performance reasons.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;defget_timeline(user_id)do
  fan_out_posts =
@feeds_table
|&amp;gt;:ets.lookup(user_id)
|&amp;gt;Enum.map(&amp;amp;elem(&amp;amp;1,1))

  celebrity_posts =
@following_table
|&amp;gt;:ets.lookup(user_id)
|&amp;gt;Enum.flat_map(fn{_actor, followed_id}-&amp;gt;
:ets.lookup(@celebrity_posts_table, followed_id)
end)
|&amp;gt;Enum.map(&amp;amp;elem(&amp;amp;1,1))

  fan_out_posts ++ celebrity_posts
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3&gt;Evaluating our dataplane&lt;a href=&quot;https://bitcrowd.dev/timelines-from-elixir#evaluating-our-dataplane&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;To get an idea about how much performance we gain with our dataplane, we evaluated it using our &lt;a href=&quot;https://bitcrowd.dev/2026/03/30/building-a-performance-evaluation-toolkit-and-a-dataplane-poc-for-atproto#the-outcomes&quot;&gt;simulator&lt;/a&gt;.
The dashboard below shows the results of this comparison.&lt;/p&gt;&lt;p&gt;We can see two blocks of data:&lt;/p&gt;&lt;p&gt;The first run was recorded with Bluesky&amp;#39;s open source dataplane.
We can see a maximum p99 latency of more than 60 ms. Furthermore, we see the throughput of requests per second fluctuating around 600.&lt;/p&gt;&lt;p&gt;In comparison, the block on the right shows recordings of the same simulation with our dataplane. The latencies are tiny, the throughput sits smoothly at 800 requests per second.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/results_simulator-c843918ee3774947ed1e5375d2f9f131.png&quot; alt=&quot;Grafana dashboard showing data for two simulation runs&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;h3&gt;Crimeline comparison&lt;a href=&quot;https://bitcrowd.dev/timelines-from-elixir#crimeline-comparison&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;a href=&quot;https://github.com/yawn/crimeline&quot;&gt;Crimeline&lt;/a&gt; is an experimental timeline builder which caught our attention when doing research for this project.
Like our proof of concept, Crimeline is not a complete dataplane implementation.
Unlike our proof of concept, Crimeline focuses on creating efficient datastructures and conveniently comes with synthetic benchmarks for them.&lt;br/&gt;After having seen the end-to-end evaluation of our dataplane, we were curious how ETS tables would perform in those benchmarks.&lt;/p&gt;&lt;p&gt;The &lt;a href=&quot;https://github.com/yawn/crimeline/blob/main/README.md&quot;&gt;README&lt;/a&gt; describes one of Crimeline&amp;#39;s datastructures, the &lt;a href=&quot;https://github.com/yawn/crimeline/blob/main/src/users/map.rs&quot;&gt;UserMap&lt;/a&gt;, as follows:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Sharded adjacency map. Each uid is split via bitmask into shard index (low bits) and backbone index (high bits). Each shard holds a &lt;code&gt;Vec&amp;lt;Vec&amp;lt;Uid&amp;gt;&amp;gt;&lt;/code&gt; — a dense backbone of sorted adjacency lists.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;It&amp;#39;s a struct that holds information whether a user ID is included in it (think your followers on Bluesky) with efficient methods to add and remove user IDs as well as performing the lookup of an ID.&lt;/p&gt;&lt;p&gt;An ETS-based equivalent looks like this:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;defmoduleUserMapEtsdo
@user0

def new do
:ets.new(:forward,[:duplicate_bag,:public,{:read_concurrency,true}])
end

defpopulated(n)do
    table =new()
    tuples =for t &amp;lt;-0..(n -1),do:{@user, t *3}
:ets.insert(table, tuples)
    table
end

defadd(table, target)do
:ets.insert(table,{@user, target})
end

defadd_bulk_list(table, targets)do
    tuples =for t &amp;lt;- targets,do:{@user, t}
:ets.insert(table, tuples)
end

defadd_bulk_each(table, targets)do
Enum.each(targets,fn t -&amp;gt;:ets.insert(table,{@user, t})end)
end

defcontains(table, target)do
:ets.match_object(table,{@user, target})!=[]
end

defremove(table, target)do
:ets.delete_object(table,{@user, target})
end

defdestroy(table)do
:ets.delete(table)
end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;We can expect this to be a bit slower than the Rust implementation of Crimeline due to the overhead of the Erlang VM.&lt;br/&gt;The table below shows the average time for each operation in &lt;strong&gt;nanoseconds&lt;/strong&gt; when performed with 100 users.&lt;br/&gt;From our results, we can see that ETS tables nevertheless perform relatively well.&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Operation&lt;/th&gt;&lt;th&gt;Rust UserMap&lt;/th&gt;&lt;th&gt;ETS UserMap&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;add&lt;/td&gt;&lt;td&gt;18 ns&lt;/td&gt;&lt;td&gt;186 ns&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;contains_hit&lt;/td&gt;&lt;td&gt;8 ns&lt;/td&gt;&lt;td&gt;1360 ns&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;contains_miss&lt;/td&gt;&lt;td&gt;8 ns&lt;/td&gt;&lt;td&gt;1350 ns&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;remove&lt;/td&gt;&lt;td&gt;26 ns&lt;/td&gt;&lt;td&gt;599 ns&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;So overall we were happy but two benchmarks stood out with growing numbers of users: &lt;code&gt;contains_hit&lt;/code&gt; and &lt;code&gt;contains_miss&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Note the units in the following table.
While the Rust UserMap goes from &lt;code&gt;8 ns&lt;/code&gt; to &lt;code&gt;14 ns&lt;/code&gt; (not even 2x) when increasing the number of users from &lt;code&gt;100&lt;/code&gt; to &lt;code&gt;100_000&lt;/code&gt;, ETS goes from &lt;code&gt;1.36 μs&lt;/code&gt; to &lt;code&gt;1.11 ms&lt;/code&gt;, almost 1000x.&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Operation (number of users)&lt;/th&gt;&lt;th&gt;Rust UserMap&lt;/th&gt;&lt;th&gt;ETS UserMap&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;contains_hit (100)&lt;/td&gt;&lt;td&gt;8 ns&lt;/td&gt;&lt;td&gt;1.36 μs&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;contains_hit (100_000)&lt;/td&gt;&lt;td&gt;14 ns&lt;/td&gt;&lt;td&gt;1.11 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;contains_miss (100)&lt;/td&gt;&lt;td&gt;8 ns&lt;/td&gt;&lt;td&gt;1.35 μs&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;contains_miss (100_000)&lt;/td&gt;&lt;td&gt;14 ns&lt;/td&gt;&lt;td&gt;1.11 ms&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;Why is this so slow?
From the &lt;a href=&quot;https://www.erlang.org/doc/apps/stdlib/ets.html&quot;&gt;ETS docs&lt;/a&gt;:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Insert and lookup times in tables of type set are constant, regardless of the table size. For table types bag and duplicate_bag time is proportional to the number of objects with the same key.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;We chose a &lt;code&gt;:duplicate_bag&lt;/code&gt; for the table to improve insert performance.
Now we are paying for that with slow lookups at scale.&lt;/p&gt;&lt;h4&gt;Roaring Bitmaps&lt;a href=&quot;https://bitcrowd.dev/timelines-from-elixir#roaring-bitmaps&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;We didn&amp;#39;t give up here, of course.
So let&amp;#39;s see what we can improve.&lt;/p&gt;&lt;p&gt;During our research, we also came across &lt;a href=&quot;https://jazco.dev/&quot;&gt;Jaz&amp;#39;s&lt;/a&gt; blog and learned how to &lt;a href=&quot;https://jazco.dev/2024/04/15/in-memory-graphs/&quot;&gt;shrink the size of your datastructures&lt;/a&gt; and go even further with &lt;a href=&quot;https://jazco.dev/2024/04/20/roaring-bitmaps/&quot;&gt;Roaring Bitmaps&lt;/a&gt;.
The latter seems to be a good fit for optimizing our user map datastructure.&lt;/p&gt;&lt;p&gt;Luckily, &lt;a href=&quot;https://hex.pm/packages/rustler&quot;&gt;rustler&lt;/a&gt; lowers the barrier to use Rust crates from Elixir a lot these days.
Conveniently for us, Aaron Gunderson already did the work to wrap the &lt;a href=&quot;https://crates.io/crates/roaring&quot;&gt;roaring crate&lt;/a&gt; in an &lt;a href=&quot;https://hex.pm/packages/roaring&quot;&gt;Elixir package&lt;/a&gt; and &lt;a href=&quot;https://agundy.com/blog/using-roaring-bitmaps-in-elixir/&quot;&gt;blogged about it&lt;/a&gt;.
So, it&amp;#39;s ready for us to use.&lt;/p&gt;&lt;p&gt;As a very brief summary, bitmaps are a suitable datastructure to compress the follower graph in our dataplane.
Instead of storing pairs of &lt;code&gt;{user_id, follower_id}&lt;/code&gt;, we store a single array for each user where each bit represents whether the user with the id at the index follows the user.&lt;/p&gt;&lt;p&gt;Say we have 5 users, and our ids start at 0. If the first four users all follow user with id 4, we would store the following pairs in our ETS table:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;# user_id, follower_id
{4, 0}
{4, 1}
{4, 2}
{4, 3}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;With a bitmap, we would store a single array of bits for the user with id 4.
If the bit is &lt;code&gt;1&lt;/code&gt;, the corresponding user follows the user with id 4.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;# user_id/index:
# 0, 1, 2, 3, 4
 [1, 1, 1, 1, 0]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;You can see how that compresses the required space in memory in this simplified example.&lt;br/&gt;Assuming 1 byte per id, we would have 4 * 2 = 8 bytes of memory in our ETS table variant.
The bitmap however is complete with 5 bits which fit in a single byte.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://roaringbitmap.org/&quot;&gt;Roaring bitmaps&lt;/a&gt; apply further optimizations on top of this basic idea.&lt;/p&gt;&lt;p&gt;With the &lt;code&gt;roaring&lt;/code&gt; package we can plug the Roaring Bitmap into our user map module.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;defmoduleUserMapRoaringdo
def new do
{:ok, roaring}=RoaringBitmap64.new()
    roaring
end

defpopulated(n)do
    list =for t &amp;lt;-0..(n -1),do: t
{:ok, roaring}=RoaringBitmap64.from_list(list)
    roaring
end

defadd(roaring, target)do
RoaringBitmap64.insert(roaring, target)
end

defadd_bulk_list(roaring, targets)do
for t &amp;lt;- targets,do:RoaringBitmap64.insert(roaring, t)
end

defadd_bulk_each(roaring, targets)do
Enum.each(targets,fn t -&amp;gt;RoaringBitmap64.insert(roaring, t)end)
end

defcontains(roaring, target)do
RoaringBitmap64.contains?(roaring, target)
end

defremove(roaring, target)do
RoaringBitmap64.remove(roaring, target)
end

defdestroy(_roaring)do
:ok
end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;With that we get the following results.&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Operation (number of users)&lt;/th&gt;&lt;th&gt;Rust UserMap&lt;/th&gt;&lt;th&gt;ETS UserMap&lt;/th&gt;&lt;th&gt;Roaring Bitmaps UserMap&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;contains_hit (100)&lt;/td&gt;&lt;td&gt;8 ns&lt;/td&gt;&lt;td&gt;1.36 μs&lt;/td&gt;&lt;td&gt;104 ns&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;contains_hit (100_000)&lt;/td&gt;&lt;td&gt;14 ns&lt;/td&gt;&lt;td&gt;1.11 ms&lt;/td&gt;&lt;td&gt;100 ns&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;contains_miss (100)&lt;/td&gt;&lt;td&gt;8 ns&lt;/td&gt;&lt;td&gt;1.35 μs&lt;/td&gt;&lt;td&gt;98 ns&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;contains_miss (100_000)&lt;/td&gt;&lt;td&gt;14 ns&lt;/td&gt;&lt;td&gt;1.11 ms&lt;/td&gt;&lt;td&gt;100 ns&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;We&amp;#39;re still slower than Crimeline&amp;#39;s implementation but it&amp;#39;s acceptable and more importantly does not degrade with the number of users.&lt;/p&gt;&lt;h4&gt;Comparison in a more realistic scenario&lt;a href=&quot;https://bitcrowd.dev/timelines-from-elixir#comparison-in-a-more-realistic-scenario&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;After satisfying our curiosity about Crimeline&amp;#39;s synthetic benchmarks, we were wondering how the comparison would look like in a more realistic scenario.
Therefore, we added another benchmark &lt;code&gt;feeds&lt;/code&gt; that is somewhere in between Crimeline&amp;#39;s benchmarks and the work we&amp;#39;ve previously done with our simulator.
Interestingly, in this more realistic scenario the performance difference between the implementations diminishes.&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Operation (number of follows)&lt;/th&gt;&lt;th&gt;Rust feed&lt;/th&gt;&lt;th&gt;ETS feed&lt;/th&gt;&lt;th&gt;Roaring Bitmaps feed&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;get_feed (100)&lt;/td&gt;&lt;td&gt;41 µs&lt;/td&gt;&lt;td&gt;9 μs&lt;/td&gt;&lt;td&gt;8 μs&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;get_feed (100_000)&lt;/td&gt;&lt;td&gt;57 µs&lt;/td&gt;&lt;td&gt;117 μs&lt;/td&gt;&lt;td&gt;108 μs&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;get_feed (1_000_000)&lt;/td&gt;&lt;td&gt;58 µs&lt;/td&gt;&lt;td&gt;118 μs&lt;/td&gt;&lt;td&gt;109 μs&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;h2&gt;Where to go from here?&lt;a href=&quot;https://bitcrowd.dev/timelines-from-elixir#where-to-go-from-here&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;We showed how to build a basic dataplane implementation in Elixir, then added optimizations to squeeze out the issues we encountered.&lt;/p&gt;&lt;p&gt;One essential feature that is missing from our implementation is persistence.
So far we store everything in memory, so our data wouldn&amp;#39;t survive a restart of the server.&lt;/p&gt;&lt;p&gt;Our goal is to provide a simple to use dataplane implementation.
So, the first idea would be to add persistence with Postgres and treat our in-memory data as an index to avoid costly queries and retrieve the data directly by primary key.
Another idea is to provide an adapter interface so that everyone can use their preferred database.&lt;/p&gt;&lt;p&gt;We&amp;#39;ve seen how to combine Rust and Elixir to get the strengths of both technologies while mitigating their weaknesses, Discord has another &lt;a href=&quot;https://discord.com/blog/using-rust-to-scale-elixir-for-11-million-concurrent-users&quot;&gt;interesting article&lt;/a&gt; on that topic.&lt;br/&gt;In essence, we can get the high level of abstraction from Elixir which leads to readable, maintainable code and a high velocity.
We can also get the parallelism and robustness of the Erlang VM on which Elixir runs.
In combination with Rust, we can improve our performance both in speed and memory usage while we are confident that the native code won&amp;#39;t crash the VM as we build on Rust&amp;#39;s memory-safety features.
This way, we can optimize the parts of the codebase where we benefit from it.
Whether it&amp;#39;s Roaring Bitmaps or even wrapping the Crimeline datastructures, a whole ecosystem of crates is available to us.&lt;/p&gt;&lt;p&gt;With that, we have all the tools to build the remaining features a complete dataplane implementation needs and we are ready to add more advanced features like the &lt;a href=&quot;https://jazco.dev/2024/04/15/in-memory-graphs/&quot;&gt;social proof&lt;/a&gt; that is available in Bluesky.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Make Friends With Your AI Assistant</title>
<link>https://rocket-science.ru/hacking/2026/05/18/make-claude-robust</link>
<enclosure type="image/jpeg" length="0" url="https://rocket-science.ru/img/logo/logo-orig.png"></enclosure>
<guid isPermaLink="false">JMW_giP8FEk1my0-Dp3QtGo1x6rwIsTFl4NlcQ==</guid>
<pubDate>Mon, 18 May 2026 10:10:08 +0000</pubDate>
<description>Several rules from my experience which would make your cooperation with AI assistants a charm</description>
<content:encoded>&lt;h2&gt;Typography&lt;/h2&gt;&lt;p&gt;AI assistants do apparently love clean markers. I use ⓪ –⑳  markers for numbering, ▸/▹ markers for itemizing, typographically correct quotes (“”/‘’,) and m-dashes. Compose key makes entering that stuff easier, alternative layout makes it a charm. No tips for poor MacOS/Win users, sorry.&lt;/p&gt;&lt;h2&gt;Workflows&lt;/h2&gt;&lt;p&gt;I have system-wide workflows for my assistant. They all look like “START_WORD → WORKFLOW_RULES → STOP_WORD.”&lt;/p&gt;&lt;p&gt;Here is an example:&lt;/p&gt;&lt;blockquote&gt;
  &lt;p&gt;Let’s define the workflow you must follow every time we work on a new task, named NEW_TASK. The rules are following:&lt;/p&gt;

  &lt;p&gt;① The statement “New task &lt;task_name&gt;!&amp;quot; typed by me starts new NEW_TASK workflow&lt;br/&gt;
② Upon starting new workflow you must pull the main branch from github and create a new branch named &amp;quot;&lt;task_name&gt;&amp;quot;&lt;br/&gt;
③ At this point you have to wait for me to enter the problem statement&lt;br/&gt;
④ You have to analyze the problem statement and immediately ask all the questions regarding it where you might have had any uncertancy and/or lack of clarity&lt;br/&gt;
⑤ After everythig is clarified, you must come up with the plan of implementation and ask for its amendment and/or approval&lt;br/&gt;
⑥ Upon approval, you should start implementing it, asking all the questions you might have appeared&lt;br/&gt;
⑦ Once done, you must create a comprehensive tests for the new functionality, documentation in both the source code as standalone (markdown), changelog entry, and a brief what-has-been-done note in markdown&lt;br/&gt;
⑧ After I accept everything by saying &amp;quot;Task done!&amp;quot;, you should commit everything with a descriptive comprehensive commit message, push it to remote, and create a pull request in github.&lt;/task_name&gt;&lt;/task_name&gt;&lt;/p&gt;
&lt;/blockquote&gt;&lt;p&gt;The key insight here is the &lt;strong&gt;stop-words&lt;/strong&gt;. Without them the assistant will cheerfully barrel through all eight steps in one breath, commit untested code to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt;, and open a pull request titled “feat: everything.” Stop-words are the leash. Love the leash (and the muzzle, your assistant is ill-mannered and disobedient.)&lt;/p&gt;&lt;h2&gt;Context Is King (And Your Assistant Has Amnesia)&lt;/h2&gt;&lt;p&gt;Every new session your assistant is a freshly hatched duckling. It will imprint on whatever you show it first. Feed it your codebase conventions, your style preferences, your testing philosophy—and do it &lt;em&gt;persistently&lt;/em&gt;, through rules and system prompts, not through hopeful repetition in every chat.&lt;/p&gt;&lt;p&gt;The difference between a rule that says “always use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:json&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:jason&lt;/code&gt;” and manually correcting the same dependency in every generated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix.exs&lt;/code&gt; is the difference between engineering and penance.&lt;/p&gt;&lt;p&gt;Persistent rules are not optional. They are the civilizational layer between you and the primordial chaos of a context window that has never heard of your project. Each workflow (see above) might have different persistent rules attached.&lt;/p&gt;&lt;h2&gt;Atomic Tasks, or: Stop Writing Novels&lt;/h2&gt;&lt;p&gt;The temptation to say “implement the entire authentication system” is roughly as productive as asking an intern to “make the app work.” Break it down. Then break it down again. If your task description exceeds one screenful, you have written a novel, not a task. If within the task there are forks in the road possible, the assistant will inevitable choose the wrong path.&lt;/p&gt;&lt;p&gt;The assistant will cheerfully &lt;em&gt;attempt&lt;/em&gt; your novel and &lt;em&gt;wild guess&lt;/em&gt; the direction on each fork. It will produce something that compiles, passes zero tests, and makes you question your career choices. Whereas “add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;verify_token/2&lt;/code&gt; function that pattern-matches on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:ok, claims}&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:error, reason}&lt;/code&gt;”—that it can do in its sleep. If models slept.&lt;/p&gt;&lt;p&gt;The rule of thumb: &lt;strong&gt;if there is more than one correct architectural path, the task is too large.&lt;/strong&gt; Decide the architecture yourself; delegate the typing.&lt;/p&gt;&lt;h2&gt;Feed It Good Code, Get Good Code&lt;/h2&gt;&lt;p&gt;This is not mysticism, it is pattern matching. If you feed your assistant examples of clean, idiomatic code, it will produce clean, idiomatic code. If you feed it your legacy spaghetti written at 3 AM during a production outage, you will get more spaghetti—but with AI-generated parmesan on top.&lt;/p&gt;&lt;p&gt;I keep a curated set of “golden” modules that I attached as context ages ago. The assistant does not need to be told “you are a senior Elixir architect with 300 years of experience.” It needs to &lt;em&gt;see&lt;/em&gt; what good Elixir looks like. Show, don’t pep-talk.&lt;/p&gt;&lt;h2&gt;Trust, But Verify (Especially the Tests)&lt;/h2&gt;&lt;p&gt;The cruelest irony of AI-assisted development: the assistant generates tests that pass. “Green across the board!” you exclaim, and deploy. Three hours later you discover the tests were testing that the function returns &lt;em&gt;something&lt;/em&gt;, not that it returns the &lt;em&gt;right&lt;/em&gt; something.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Read the generated tests more carefully than the implementation.&lt;/strong&gt; The implementation at least looks suspicious when it is wrong. A bad test looks &lt;em&gt;exactly&lt;/em&gt; like a good test, except it asserts nothing useful. Pattern-match on the actual values; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert result&lt;/code&gt; is not a test, it is a prayer.&lt;/p&gt;&lt;h2&gt;The Art of Saying “No, Try Again”&lt;/h2&gt;&lt;p&gt;Your assistant is not your therapist. It does not need encouragement. “This is wrong” followed by silence teaches it nothing. “This is wrong because the recursive clause does not handle the empty-list base case, see &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;List.foldl/3&lt;/code&gt; for the pattern I expect”—now you are getting somewhere. Keep your paw right above &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ctrl&lt;/code&gt;+&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C&lt;/code&gt; combination and press it every time you see the assistant’s thinking process drives bonkers.&lt;/p&gt;&lt;p&gt;You always liked interrupting people in person, feel free to interrupt your deputy.&lt;/p&gt;&lt;h2&gt;Know When to Kill the Session&lt;/h2&gt;&lt;p&gt;If you have been going back and forth for more than three iterations on the same function, close the session. Start fresh. The context window is polluted with failed attempts, corrections, and mutual disappointment—a couples therapy transcript, essentially.&lt;/p&gt;&lt;p&gt;A new session with a clean description of what you &lt;em&gt;actually&lt;/em&gt; want will outperform the seventh round of “no, I meant the &lt;em&gt;other&lt;/em&gt; thing” every single time. Sunk cost fallacy applies to token budgets too.&lt;/p&gt;&lt;p&gt;Please aware, that the necessity to close a session vividly implies your original prompt sucked in the first place. Rewrite it from scratch. Make sure it has no freedom for the assistant to choose paths. They are good at walking the straight roads.&lt;/p&gt;&lt;h2&gt;The Checklist&lt;/h2&gt;&lt;p&gt;Distilled to a grocery list for the impatient:&lt;/p&gt;&lt;p&gt;▸ Use persistent rules, not per-session rituals&lt;br/&gt;
▸ Define workflows with explicit start/stop words&lt;br/&gt;
▸ One architectural decision per task, maximum&lt;br/&gt;
▸ Attach good code as examples, skip the motivational preamble&lt;br/&gt;
▸ Run the full validation pipeline (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;format&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;credo&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dialyzer&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt;) after every step, not at the end&lt;br/&gt;
▸ Read generated tests as if they were written by someone who wants to trick you (they were not, but the effect is the same)&lt;br/&gt;
▸ Three failed iterations = kill the session, rethink the task&lt;br/&gt;
▸ Never let the assistant commit without your explicit say-so&lt;br/&gt;
▸ If you could not solve the problem yourself, the assistant cannot solve it &lt;em&gt;for&lt;/em&gt; you—it can only type faster&lt;/p&gt;&lt;p&gt;The last point deserves a tattoo. An AI assistant is a force multiplier, not a force. Multiply zero by anything you like.&lt;/p&gt;&lt;p&gt;Happy prompting.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Mocks Are Your Friends, Not Your Servants</title>
<link>https://rocket-science.ru/hacking/2026/05/17/mocks-are-your-friends</link>
<enclosure type="image/jpeg" length="0" url="https://rocket-science.ru/img/logo/logo-orig.png"></enclosure>
<guid isPermaLink="false">24nWcNfiDTqnapHrTllRG-SjwLMwHSxIpCUCdA==</guid>
<pubDate>Sun, 17 May 2026 18:03:59 +0000</pubDate>
<description>On treating mocks as protagonists of the testing narrative rather than disposable stand-ins hired to nod along</description>
<content:encoded>&lt;p&gt;There is a peculiar tradition in our industry: the moment someone says “mock,” half the room recoils as if you have just proposed replacing the database with a spreadsheet. The other half nods enthusiastically, having already replaced the database with a spreadsheet.&lt;/p&gt;&lt;p&gt;Both camps are wrong, and for the same reason: they think of “mock” as a verb.&lt;/p&gt;&lt;h2&gt;Mock Is a Noun&lt;/h2&gt;&lt;p&gt;José Valim wrote a &lt;a href=&quot;https://dashbit.co/blog/mocks-and-explicit-contracts&quot;&gt;splendid piece on the subject&lt;/a&gt; some years ago, and yet the industry continues to collectively ignore its central thesis with the dedication of a cat ignoring an expensive toy. The key insight is disarmingly simple: &lt;strong&gt;consider “mock” to be a noun, never a verb.&lt;/strong&gt; You do not “mock a module.” You create a mock—an entity, a collaborator, a contract participant—that implements a well-defined behaviour.&lt;/p&gt;&lt;p&gt;The difference is not cosmetic. When you “mock” a function (verb), you are lying to your test. You are saying: “pretend this hole in the wall is a door.” When you create a mock (noun), you are building an actual door—one that opens and closes according to the same interface contract as the production door, just perhaps into a smaller room.&lt;/p&gt;&lt;h2&gt;Stubs Are for Candles, Not for Software&lt;/h2&gt;&lt;p&gt;The prevailing use of mocks in the wild amounts to: “I need this function to return 42 so my other function does not crash.” This is not testing. This is bribery. You have paid off a witness to give the testimony you wanted, and now you are surprised when the real trial goes sideways.&lt;/p&gt;&lt;p&gt;A mock that merely returns canned responses is a stub wearing a fake moustache. It tells you nothing about whether your code actually &lt;em&gt;interoperates&lt;/em&gt; with the expected behaviour. It only tells you that, given a universe where everything behaves exactly as you imagined, your code does not crash. Congratulations—that was never the hard part.&lt;/p&gt;&lt;p&gt;The hard part is the contract. Does your code send the right messages? Does it handle the responses defined by the behaviour? Does it &lt;em&gt;respect the protocol&lt;/em&gt;, not just survive it? A proper mock enforces these questions. A stub sweeps them under the carpet and charges you for the cleaning.&lt;/p&gt;&lt;h2&gt;Promote Your Mocks to Lead Actors&lt;/h2&gt;&lt;p&gt;Here is the heresy: mocks should not sit quietly in the corner of your test, responding when spoken to like well-trained waitstaff. They should be protagonists. They should &lt;em&gt;drive&lt;/em&gt; the testing narrative.&lt;/p&gt;&lt;p&gt;Consider a finite state machine. It transitions through states, fires callbacks, notifies listeners. In production, the listener might persist data, send emails, ring bells. In tests, you do not care about the bells. You care about: did the FSM reach state X with payload Y after event Z?&lt;/p&gt;&lt;p&gt;A mock-as-protagonist answers this question directly. It is not merely absorbing calls—it is &lt;em&gt;reporting back&lt;/em&gt; to the test, asserting that the system did what it promised. The mock becomes your embedded journalist, filing reports from inside the process under test.&lt;/p&gt;&lt;p&gt;This is a fundamentally different posture. The mock is no longer a shift worker you hire to stand in for the real employee. It is a first-class participant in the test, with its own responsibilities, its own assertions, its own voice.&lt;/p&gt;&lt;h2&gt;OTP, Race Conditions, and the Debugger You Deserve&lt;/h2&gt;&lt;p&gt;Anyone who has tested concurrent OTP systems knows the special joy of a test that passes ninety-nine times and fails on the hundredth, always at random time, always on CI, never on your machine. The core problem is structural: you fire an asynchronous message and then try to assert about a state that may or may not have been reached yet. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Process.sleep(200)&lt;/code&gt; is not a solution—it is a prayer in milliseconds.&lt;/p&gt;&lt;p&gt;Mocks offer something better. When a mock is registered as a listener, every state transition sends a message back to the test process. You can then use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert_receive&lt;/code&gt; to &lt;em&gt;wait&lt;/em&gt; for the transition, with a timeout, deterministically. No sleep. No prayer. No coin flip.&lt;/p&gt;&lt;p&gt;In effect, the mock becomes a breakpoint in a debugger you never had to open. It declares: “I expect to be called with &lt;em&gt;these&lt;/em&gt; arguments, in &lt;em&gt;this&lt;/em&gt; order,” and it sends a message to the test process confirming each call. You get both the expectation (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;expect/3&lt;/code&gt;) and the synchronization (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert_receive/2&lt;/code&gt;) in one mechanism. The mock is simultaneously the probe and the signal.&lt;/p&gt;&lt;p&gt;This is not theoretical. In OTP, where processes communicate via message passing and the scheduler is free to interleave execution in whatever order amuses it most, this pattern transforms flaky tests into deterministic ones. You are no longer guessing when the process reached the desired state. You are being &lt;em&gt;told&lt;/em&gt;.&lt;/p&gt;&lt;h2&gt;Listeners, Visibility, and the Joy of Seeing What Happens&lt;/h2&gt;&lt;p&gt;There is a secondary benefit that deserves its own paragraph: visibility.&lt;/p&gt;&lt;p&gt;When you plug a mock in as a listener—not as a replacement for an implementation, but as an &lt;em&gt;observer&lt;/em&gt; of one—the test code becomes radically more readable. Each assertion is a statement about what the system &lt;em&gt;did&lt;/em&gt;, not about what you had to simulate. The reader does not need to mentally reconstruct the production flow from a pile of stubs. The flow is right there, reported by the mock, step by step.&lt;/p&gt;&lt;p&gt;Test code that reads like a narrative of actual system behaviour is test code that people trust. And test code that people trust is test code that people maintain. And test code that people maintain is test code that catches bugs. The chain of causation is longer than a Dickensian sentence, but every link holds.&lt;/p&gt;&lt;h2&gt;Finitomata: A Case Study in Mock-Driven Testing&lt;/h2&gt;&lt;p&gt;All of this is not armchair philosophy. The &lt;a href=&quot;https://github.com/am-kantox/finitomata/blob/main/lib/finitomata/test/ex_unit.ex&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Finitomata.ExUnit&lt;/code&gt;&lt;/a&gt; module is a working testing framework built entirely on this premise. It uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mox&lt;/code&gt; not as a crutch but as the foundation.&lt;/p&gt;&lt;p&gt;The setup declares a mock listener. The mock is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;allow&lt;/code&gt;-ed to the FSM process and given &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;expect&lt;/code&gt;ations for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;after_transition/3&lt;/code&gt; callbacks. Each expectation sends &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{:on_transition, id, state, payload}&lt;/code&gt; back to the test process. The test then walks the FSM through its transitions and asserts each state deterministically via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert_receive&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;The result looks like this:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;test_path &amp;quot;respectful passenger&amp;quot;, %{passengers: initial_passengers} do
  :coin_in -&amp;gt;
    assert_state :opened do
      assert_payload do
        data.passengers ~&amp;gt; ^initial_passengers
      end
    end

  :walk_in -&amp;gt;
    assert_state :closed do
      assert_payload do
        data.passengers ~&amp;gt; one_more when one_more == 1 + data.passengers
      end
    end

  :switch_off -&amp;gt;
    assert_state :switched_off
    assert_state :*
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;No &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Process.sleep&lt;/code&gt;. No polling. No race conditions. Each transition is confirmed by the mock-listener before the test proceeds. And crucially: &lt;em&gt;intermediate&lt;/em&gt; states—including those triggered by automatic (bang!) transitions that the test never explicitly fires—are observable and assertable, because the mock reports every single one.&lt;/p&gt;&lt;p&gt;The mock here is not a stand-in. It is the entire testing infrastructure. Remove it, and you are back to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Process.sleep(200)&lt;/code&gt; and crossed fingers.&lt;/p&gt;&lt;h2&gt;The Moral&lt;/h2&gt;&lt;p&gt;Mocks have suffered from a branding problem. They were introduced to the mainstream as “a way to avoid calling the real thing,” which is roughly equivalent to introducing a violin as “a way to avoid silence.” Technically correct, but missing the point so thoroughly that it constitutes its own genre of wrongness.&lt;/p&gt;&lt;p&gt;A mock is a collaborator. A contract enforcer. A synchronisation primitive. A visibility layer. A debugger breakpoint. A protagonist.&lt;/p&gt;&lt;p&gt;Treat it accordingly, and your tests will repay you with the one thing no amount of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Process.sleep&lt;/code&gt; can buy: confidence.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Passkey Sign In with Elixir and Phoenix</title>
<link>https://shadowfacts.net/2023/phoenix-passkeys/</link>
<enclosure type="image/jpeg" length="0" url="https://shadowfacts.net/shadowfacts.png"></enclosure>
<guid isPermaLink="false">VczlMu0F-895eAWwo-kF-ZStbBe9ejiafL56TQ==</guid>
<pubDate>Fri, 08 May 2026 09:29:25 +0000</pubDate>
<description>The outer part of a shadow is called the penumbra.</description>
<content:encoded>&lt;p&gt;Passkeys are a replacement for passwords that use public/private cryptographic key pairs for login in a way that can be more user-friendly and resistant to a number of kinds of attacks. Let’s implement account registration and login with passkeys in a simple Phoenix web app.&lt;/p&gt;&lt;p&gt;This work is heavily based on &lt;a href=&quot;https://www.imperialviolet.org/2022/09/22/passkeys.html&quot;&gt;this article&lt;/a&gt; by Adam Langley, which provides a great deal of information about implementing passkeys. My goal here is to fill in some more of the details, and provide some Elixir-specific information. As with Adam’s article, I’m not going to use any WebAuthn libraries (even though that may be advisable from a security/maintenance perspective) since I think it’s interesting and helpful to understand how things actually work.&lt;/p&gt;&lt;p&gt;Providing an exhaustive, production-ready implementation is a non-goal of this post. I’m going to make some slightly odd decisions for pedagogical reasons, and leave some things incomplete. That said, I’ll try to note when I’m doing so.&lt;/p&gt;&lt;p&gt;To start, I’m using the default Phoenix template app (less Tailwind) in which I’ve also generated the default, controller-based &lt;a href=&quot;https://hexdocs.pm/phoenix/mix_phx_gen_auth.html&quot;&gt;authentication system&lt;/a&gt;. Some parts of the password-specific stuff have been stripped out altogether, others will get changed to fit with the passkey authentication setup we’re going to build.&lt;/p&gt;&lt;h2&gt;Database schema §&lt;/h2&gt;&lt;p&gt;The first thing we’ll need is a backend schema for passkeys. The only data we need to store for a particular passkey is a unique identifier, and the public key itself. We also want users to be able to have multiple passkeys associated with their account (since they may want to login from multiple platforms that don’t cross-sync passkeys), so the users schema will have a one-to-many relationship with the passkeys.&lt;/p&gt;&lt;p&gt;The actual model I’ll call &lt;code&gt;UserCredential&lt;/code&gt;, since that’s closer to what the WebAuthn spec calls them. Here’s the schema, it’s pretty simple:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeys.Accounts.UserCredential do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, :binary, []}

  schema &amp;quot;users_credentials&amp;quot; do
    # DER-encoded Subject Public Key Info: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.7
    field :public_key_spki, :binary
    belongs_to :user, PhoenixPasskeys.Accounts.User
    timestamps()
  end

  def changeset(credential, attrs) do
    credential
    |&amp;gt; cast(attrs, [:id, :public_key_spki, :user_id])
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;There are a couple things to note here:&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;First, we’re explicitly specifying that the primary key is a binary, since that’s what the WebAuthn API provides as the credential ID.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;public_key_spki&lt;/code&gt; field contains the data for the public key itself and the algorithm being used. We don’t care about the specific format, though, since we don’t have to parse it ourselves.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;To the generated &lt;code&gt;User&lt;/code&gt; schema, I also added the &lt;code&gt;has_many&lt;/code&gt; side of the relationship. There’s also a migration to create the credentials table. I won’t show it here, since it’s exactly what you’d expect—just make sure the &lt;code&gt;id&lt;/code&gt; column is a binary.&lt;/p&gt;&lt;h2&gt;Registration JavaScript §&lt;/h2&gt;&lt;p&gt;WebAuthn, being a modern web API, is a JavaScript API. This is a distinct disadvantage, if your users expect to be able to login from JavaScript-less browsers. Nonetheless, we proceed with the JS. Here’s the first bit:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;document.addEventListener(&amp;quot;DOMContentLoaded&amp;quot;, () =&amp;gt; {
    const registrationForm = document.getElementById(&amp;quot;registration-form&amp;quot;);
    if (registrationForm) {
        registrationForm.addEventListener(&amp;quot;submit&amp;quot;, (event) =&amp;gt; {
            event.preventDefault();
            registerWebAuthnAccount(registrationForm);
        });
    }
});&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If we find the registration &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element (note that I’ve added an ID (and also removed the password field)), we add a submit listener to it which prevents the form submission and instead calls the &lt;code&gt;registerWebAuthnAccount&lt;/code&gt; function we’ll create next.&lt;/p&gt;&lt;p&gt;Before we get there, we’ll also write a brief helper function to check whether passkeys are actually available:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;async function supportsPasskeys() {
    if (!window.PublicKeyCredential || !PublicKeyCredential.isConditionalMediationAvailable) {
		return false;
	}
	const [conditional, userVerifiying] = await Promise.all([
		PublicKeyCredential.isConditionalMediationAvailable(),
		PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),
	]);
	return conditional &amp;amp;&amp;amp; userVerifiying;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The conditional mediation check establishes that WebAuthn conditional UI is available. This is what we’ll use for login, and is part of what makes using passkeys a nice, slick experience. Conditional UI will let us start a request for a passkey that doesn’t present anything until the user interacts with the browser’s passkey autofill UI.&lt;/p&gt;&lt;p&gt;The user-verifying platform authenticator check establishes that, well, there is a user-verifying platform authenticator. The platform authenticator part means an authenticator that’s part of the user’s device, not removable, and the user-verifying part means that the authenticator verifies the presence of the user (such as via biometrics).&lt;/p&gt;&lt;p&gt;In the &lt;code&gt;registerWebAuthnAccount&lt;/code&gt; function, the first thing we need to do is check that both these conditions are met and passkeys are supported by the browser. If not, we’ll just bail out and registration won’t be possible.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;async function registerWebAuthnAccount(form) {
    if (!(await supportsPasskeys())) {
        return;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, we’ll setup a big ol’ options object that we’ll pass to the WebAuthn API to specify what sort of credential we want. There’s going to be a whole lot of stuff here, so lets take it one piece at a time.&lt;/p&gt;&lt;p&gt;The RP is the Relying Party—that is, us, the party which relies on the credentials for authentication. The ID is the &lt;a href=&quot;https://w3c.github.io/webauthn/#rp-id&quot;&gt;domain of the RP&lt;/a&gt;—just localhost for this example, though in reality you’d need to use your actual domain in production. The name is just a user-facing name for the RP.&lt;/p&gt;&lt;p&gt;Next up is some info about the user who the credential is for. The user’s “name” will just be the email that they entered in the form. The &lt;code&gt;displayName&lt;/code&gt; value is required, per the spec, but we don’t have any other information so we just leave it blank and let the browser display the &lt;code&gt;name&lt;/code&gt; only. The ID is where this gets a little weird, since we’re just generating a random 64-byte value:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;function generateUserID() {
	const userID = new Uint8Array(64);
	crypto.getRandomValues(userID);
	return userID;
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you were adding a passkey to an existing user account, you could use a server-specified value for the user ID and the browser would replace any existing credentials with the same user ID and RP ID. But, since the user is registering a new account at this point, we assume they don’t want to do that (and, moreover, we don’t have any ID we can use yet). The user ID will also be returned upon a successful login, which would let us look up the user that’s logging in. However, since we’re only going to allow a credential to belong to a single user, we can use the credential’s ID to uniquely determine the user.&lt;/p&gt;&lt;p&gt;Next up, we specify the types of public keys that we’ll accept. We’re going to support Ed25519, ES256, and RS256, since those are recommended by the WebAuthn spec:&lt;/p&gt;&lt;p&gt;During the login process, the server will generate a challenge value that the client will sign and the server will verify was signed with the user’s private key. The spec also requires that we provide a challenge when creating a credential, but we’re not going to use it for anything (since the user is just now creating the credential, we have nothing trusted that we can verify the initial challenge against), so we just provide an empty value:&lt;/p&gt;&lt;p&gt;Lastly, we give our requirements for authenticators that we want to use:&lt;/p&gt;&lt;p&gt;We want only the platform authenticator, not anything else, like removable security keys. We also specify that we want a resident key. This usage of “resident” is deprecated terminology that’s enshrined in the API. Really it means we want a &lt;em&gt;discoverable&lt;/em&gt; credential, so that the authenticator will surface it during the login process without us having to request the credential by ID. This is important to note, since it’s what prevents needing a separate username entry step.&lt;/p&gt;&lt;p&gt;Now that we (finally) have all the configuration options in place, we can actually proceed with the credential creation. We pass the options to &lt;code&gt;navigator.credentials.create&lt;/code&gt; to actually create the WebAuthn credential. If that fails, we’ll just take the easy way out and alert to inform the user (in an actual service, you’d probably want better error handling).&lt;/p&gt;&lt;p&gt;From the credential we get back, we need a few pieces of information. First is the decoded client data, which is an object that contains information about the credential creation request that occurred.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;clientDataJSON&lt;/code&gt; field of the response object contains the JSON-serialized object in an &lt;code&gt;ArrayBuffer&lt;/code&gt;, so we decode that to text and then parse the JSON. With the decoded object, we do a consistency check with a couple pieces of data: the type of the request, whether or not it was cross-origin, and the actual origin being used.&lt;/p&gt;&lt;p&gt;If it was not a creation request, the request was cross-origin, or the origin doesn’t match, we bail out. Note that in production, the origin should be checked against the actual production origin, not localhost. And again, in reality you’d want better error handling than just an alert.&lt;/p&gt;&lt;p&gt;Next, we need to get the authenticator data which is encoded in a binary format and pull a few pieces of data out of it. You can see the full format of the authenticator data &lt;a href=&quot;https://w3c.github.io/webauthn/#sctn-authenticator-data&quot;&gt;in the spec&lt;/a&gt;, but the parts we’re interested in are the backed-up state and the credential ID, which is part of the &lt;a href=&quot;https://w3c.github.io/webauthn/#attested-credential-data&quot;&gt;attested credential data&lt;/a&gt;.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;async function registerWebAuthnAccount(form) {
    // ...
    const authenticatorData = new Uint8Array(credential.response.getAuthenticatorData());
    const backedUp = (authenticatorData[32] &amp;gt;&amp;gt; 4) &amp;amp; 1;
    const idLength = (authenticatorData[53] &amp;lt;&amp;lt; 8) | authenticatorData[54];
    const id = authenticatorData.slice(55, 55 + idLength);
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We get the backed-up bit from the flags byte at offset 32. Then we get the length of the credential ID, which is encoded as a big-endian, 16-bit integer at bytes 53 and 54. The ID itself immediately follows the length, thus starting at byte 55.&lt;/p&gt;&lt;p&gt;Before proceeding, we check that the &lt;a href=&quot;https://w3c.github.io/webauthn/#sctn-credential-backup&quot;&gt;backed-up bit&lt;/a&gt; is set, indicating that the credential is backed-up and safe from the user losing the current device. If it’s not, we won’t let the user register with this passkey. I choose to do this since it’s recommended by Adam Langley’s blog post, but whether it’s actually necessary may depend on your specific circumstances.&lt;/p&gt;&lt;p&gt;The last piece of data we need out of the credential is the actual public key:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;async function registerWebAuthnAccount(form) {
    // ...
    const publicKey = credential.response.getPublicKey();
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And with all that in place, we can actually initiate the registration. We’ll assemble a form data payload with all of the requisite values:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;async function registerWebAuthnAccount(form) {
    // ...
    const body = new FormData();
    body.append(&amp;quot;_csrf_token&amp;quot;, form._csrf_token.value);
    body.append(&amp;quot;email&amp;quot;, form[&amp;quot;user[email]&amp;quot;].value);
    body.append(&amp;quot;credential_id&amp;quot;, arrayBufferToBase64(id));
    body.append(&amp;quot;public_key_spki&amp;quot;, arrayBufferToBase64(publicKey));
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We’ll use the body in a POST request to the registration endpoint. The response we get back will contain a status value to indicate whether the request was successful or not. If it was successful, the backend will have set the session cookie, and we can redirect to the home page and the new user will be logged in. If registration failed, we’ll alert the user.&lt;/p&gt;&lt;p&gt;And with that, we’re done with JavaScript (for now) and can move on to the backend part of registering an account.&lt;/p&gt;&lt;h2&gt;Creating a user §&lt;/h2&gt;&lt;p&gt;In the &lt;code&gt;UserRegistrationController&lt;/code&gt; module that comes with the Phoenix auth template, we’ll change the &lt;code&gt;create&lt;/code&gt; function. By default, it registers the user using the parameters from the signup form and then redirects to the homepage. Instead, we’re going to register using the passkey we created on the client and then respond with the JSON that our JavaScript is expecting.&lt;/p&gt;&lt;p&gt;The first thing we need to do is extract the values that were sent by the frontend and decode the base 64-encoded ones.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeysWeb.UserRegistrationController do
  def create(conn, %{
        &amp;quot;email&amp;quot; =&amp;gt; email,
        &amp;quot;credential_id&amp;quot; =&amp;gt; credential_id,
        &amp;quot;public_key_spki&amp;quot; =&amp;gt; public_key_spki
      }) do
    credential_id = Base.decode64!(credential_id)
    public_key_spki = Base.decode64!(public_key_spki)
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Those get passed to the &lt;code&gt;Accounts.register_user&lt;/code&gt; function, which we’ll update shortly to handle. If account creation succeeded, we’ll still send the confirmation email as the existing code did. After that, instead of redirecting, we’ll log the user in by setting the session cookie and then respond with the “ok” status for the frontend. If account creation fails, we’ll just respond with the “error” status so the frontend can alert the user.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeysWeb.UserRegistrationController do
  def create(...) do
    # ...
    case Accounts.register_user(email, credential_id, public_key_spki) do
      {:ok, user} -&amp;gt;
        # Send confirmation email...

        conn
        |&amp;gt; UserAuth.log_in_user_without_redirect(user)
        |&amp;gt; json(%{status: :ok})

      {:error, _changeset} -&amp;gt;
        json(conn, %{status: :error})
    end
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Let’s update the &lt;code&gt;register_user&lt;/code&gt; function. Instead of just creating a changeset for the user and then inserting it, we need to also create the authenticator. To avoid potentially leaving things in a broken state, we wrap both of these in a database transaction.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeys.Accounts do
  def register_user(email, credential_id, public_key_spki) do
    Repo.transaction(fn -&amp;gt;
      user =
        %User{}
        |&amp;gt; User.registration_changeset(%{email: email})
        |&amp;gt; Repo.insert()
        |&amp;gt; case do
          {:ok, user} -&amp;gt; user
          {:error, changeset} -&amp;gt; Repo.rollback(changeset)
        end
    end)
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;First, we create a user with the given email. If the user creation fails, we abort and rollback the transaction. Then, we can create a credential belonging to the new user with the credential ID and public key we received from the client. As with the user, if creating the credential fails, we rollback the transaction.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeys.Accounts do
  def register_user(email, credential_id, public_key_spki) do
    Repo.transaction(fn -&amp;gt;
      # ...
      %UserCredential{}
      |&amp;gt; UserCredential.changeset(%{
        id: credential_id,
        public_key_spki: public_key_spki,
        user_id: user.id
      })
      |&amp;gt; Repo.insert()
      |&amp;gt; case do
        {:ok, _credential} -&amp;gt; nil
        {:error, changeset} -&amp;gt; Repo.rollback(changeset)
      end
    end)
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We don’t need to do anything with the newly created credential, so we can just ignore it once it’s been created.&lt;/p&gt;&lt;p&gt;And finally, from the transaction function, we return the user:&lt;/p&gt;&lt;p&gt;The last thing we need to to complete the registration flow is to set the new user’s session cookie so that they’re logged in immediately. The &lt;code&gt;UserAuth&lt;/code&gt; module that’s generated as part of the Phoenix auth template has a &lt;code&gt;log_in_user&lt;/code&gt; function that does exactly this. But it also redirects the connection to another endpoint. We don’t want to do that, since we’re sending a JSON response, so I’ve split the function into two: one that only sets the session, and the existing function that sets the session and then redirects.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeysWeb.UserAuth do
  def log_in_user(conn, user, params \\ %{}) do
    user_return_to = get_session(conn, :user_return_to)
    conn
    |&amp;gt; log_in_user_without_redirect(user, params)
    |&amp;gt; redirect(to: user_return_to || signed_in_path(conn))
  end

  def log_in_user_without_redirect(conn, user, params \\ %{}) do
    token = Accounts.generate_user_session_token(user)
    conn
    |&amp;gt; renew_session()
    |&amp;gt; put_token_in_session(token)
    |&amp;gt; maybe_write_remember_me_cookie(token, params)
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And with that, everything is in place and you can now create an account with a passkey!&lt;/p&gt;&lt;h2&gt;Login form §&lt;/h2&gt;&lt;p&gt;Now that the user’s got an account, they need to be able to login with it. That means once again interacting with the WebAuthn API and writing a bunch of JavaScript. But before we get there, we need some slight changes to the backend.&lt;/p&gt;&lt;p&gt;In the HTML for the login page, we’ll add an ID to the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element so that we can find it from JS. We’ll also remove the password field, which is obviously no longer necessary. Lastly, but certainly not least, we need to send a challenge to the client.&lt;/p&gt;&lt;p&gt;The challenge is a value that the user’s device will cryptographically sign with their private key. The result will get sent back to the server, and we’ll verify it against the public key we have stored thus authenticating them. We’ll send the challenge just in a hidden form field:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&amp;quot;hidden&amp;quot; id=&amp;quot;challenge&amp;quot; value={@webauthn_challenge} /&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the session controller, we’ll need to generate and assign the challenge to the connection.&lt;/p&gt;&lt;p&gt;WebAuthn expects the challenge to be a value up to 64 bytes long, so we’ll use the Erlang crypto module to generate one of that length. The value is encoded as &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc4648#section-5&quot;&gt;URL-safe base 64&lt;/a&gt; (the same as normal base 64, but with dash and underscore rather than plus and slash) without padding. We encode it this way since that’s the format in which it will later be &lt;a href=&quot;https://w3c.github.io/webauthn/#dom-collectedclientdata-challenge&quot;&gt;returned&lt;/a&gt; as part of the &lt;code&gt;clientDataJSON&lt;/code&gt;, so when we extract that value we can directly compare it to the challenge value we generated.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeysWeb.UserSessionController do
  defp put_webauthn_challenge(conn) do
    challenge =
      :crypto.strong_rand_bytes(64)
      |&amp;gt; Base.url_encode64(padding: false)

    conn
    |&amp;gt; put_session(:webauthn_challenge, challenge)
    |&amp;gt; assign(:webauthn_challenge, challenge)
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note that the challenge string is also stored in the session, so that we can later check that the challenge that the client signed matches the challenge we generated. It’s safe to store this in the session, even though it’s sent to the client, because the cookie is encrypted and signed so the client can’t tamper with it.&lt;/p&gt;&lt;h2&gt;Login JavaScript §&lt;/h2&gt;&lt;p&gt;With the first part of the backend changes taken care of, it’s time for more JavaScript, baby!&lt;/p&gt;&lt;p&gt;We’ll follow a similar outline to the registration setup (and the same caveat applies about error handling).&lt;/p&gt;&lt;pre&gt;&lt;code&gt;document.addEventListener(&amp;quot;DOMContentLoaded&amp;quot;, () =&amp;gt; {
    // ...
    const loginForm = document.getElementById(&amp;quot;login-form&amp;quot;);
	if (loginForm) {
        loginWebAuthnAccount(loginForm);
    }
});

async function loginWebAuthnAccount(loginForm) {
    if (!(await supportsPasskeys())) {
        return;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first thing we need to do is grab the challenge from the hidden form field, then we can construct the options object for getting the credential (which is, thankfully, much simpler than for creation).&lt;/p&gt;&lt;p&gt;In the options, we specify that we want conditional mediation. As noted before, this means that the browser won’t display any UI, except for autofill, for this credential request until the user accepts the autofill suggestion. In the public key options, we also give the decoded challenge value and specify the our Relying Party ID (again, this would need to be the actual domain in production).&lt;/p&gt;&lt;p&gt;Now, we can actually make the credential request and then, if we get a credential back, encode and send all the values to the backend. We need to send the ID of the credential, so that the backend can find its public key and the corresponding user. We also need the client data JSON, which we send as text decoded from the &lt;code&gt;ArrayBuffer&lt;/code&gt; it’s returned as. We also need to send the authenticator data as well as the signature itself.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;async function loginWebAuthnAccount(loginForm) {
    // ...
    const credential = await navigator.credentials.get(getOptions);
    if (!credential) {
        alert(&amp;quot;Could not get credential&amp;quot;);
        return;
    }

    const clientDataJSON = new TextDecoder().decode(credential.response.clientDataJSON);

    const body = new FormData();
    body.append(&amp;quot;_csrf_token&amp;quot;, loginForm._csrf_token.value);
    body.append(&amp;quot;raw_id&amp;quot;, arrayBufferToBase64(credential.rawId));
    body.append(&amp;quot;client_data_json&amp;quot;, clientDataJSON);
    body.append(&amp;quot;authenticator_data&amp;quot;, arrayBufferToBase64(credential.response.authenticatorData));
    body.append(&amp;quot;signature&amp;quot;, arrayBufferToBase64(credential.response.signature));
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We can then send a request to the login endpoint. If the backend request fails (or if we failed to get the credential) we just alert the user (again, you’d probably want something better in reality). If the login attempt was successful, the server will have set the session cookie, and so we can just redirect to the homepage and the user will be logged in.&lt;/p&gt;&lt;p&gt;With that in place, let’s move on to the backend half of the login request.&lt;/p&gt;&lt;h2&gt;Validating a login attempt §&lt;/h2&gt;&lt;p&gt;As with signup, we’ll modify the existing log in endpoint to actually validate the WebAuthn login attempt.&lt;/p&gt;&lt;p&gt;The first step is extracting all of the information the frontend provides in the params and decoding it:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeysWeb.UserSessionController do
  def create(conn, params) do
    id = params |&amp;gt; Map.get(&amp;quot;raw_id&amp;quot;) |&amp;gt; Base.decode64!()
    authenticator_data = params |&amp;gt; Map.get(&amp;quot;authenticator_data&amp;quot;) |&amp;gt; Base.decode64!()
    client_data_json_str = params |&amp;gt; Map.get(&amp;quot;client_data_json&amp;quot;)
    signature = params |&amp;gt; Map.get(&amp;quot;signature&amp;quot;) |&amp;gt; Base.decode64!()
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, we’re going to validate all of the information we got from the client. Before we get to that, there are a handful of helper functions we’ll use. First, looking up a credential by its ID:&lt;/p&gt;&lt;p&gt;Next, a function in the controller that verifies the signature against the provided data:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeysWeb.UserSessionController do
  defp verify_signature(credential, client_data_json_str, authenticator_data, signature) do
    with {:ok, pubkey} &amp;lt;- X509.PublicKey.from_der(credential.public_key_spki),
         client_data_json_hash &amp;lt;- :crypto.hash(:sha256, client_data_json_str),
         signed_message &amp;lt;- authenticator_data &amp;lt;&amp;gt; client_data_json_hash,
         true &amp;lt;- :public_key.verify(signed_message, :sha256, signature, pubkey) do
      true
    else
      _ -&amp;gt;
        false
    end
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;X509&lt;/code&gt; comes from the &lt;a href=&quot;https://hex.pm/packages/x509&quot;&gt;&lt;code&gt;x509&lt;/code&gt;&lt;/a&gt; package, which is the only third-party piece of code we’re using. It’s a fairly thin wrapper around the Erlang &lt;code&gt;public_key&lt;/code&gt; and &lt;code&gt;crypto&lt;/code&gt; modules, and mostly serves to save me from having to deal with Erlang records in my code. Its &lt;code&gt;from_der&lt;/code&gt; helper function is used to parse the public key from the encoded format.&lt;/p&gt;&lt;p&gt;Next, we hash the client data JSON and append that hash to the authenticator data from the client. This value is what should match the signature using the public key we’ve got, so finally we check that. If all these steps succeeded, we return true, and false otherwise.&lt;/p&gt;&lt;p&gt;The last helper function will receive the decoded client data make sure it’s got all of the values that we expect. If the &lt;code&gt;crossOrigin&lt;/code&gt; value is present and is not false, the client data is invalid and the login attempt will be rejected.&lt;/p&gt;&lt;p&gt;Otherwise, we check that the data has the expected type and origin, and we extract the challenge value (note that we’re checking the origin again here, and this would need to change in production):&lt;/p&gt;&lt;p&gt;And lastly, if neither of the previous patterns matched, the client data fails validation:&lt;/p&gt;&lt;p&gt;Now, let’s put all those parts together and validate the login attempt.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeysWeb.UserSessionController do
  def create(conn, params) do
    # ...
    with credential when not is_nil(credential) &amp;lt;- Accounts.get_credential(id),
         true &amp;lt;- verify_signature(credential, client_data_json_str, authenticator_data, signature),
         {:ok, client_data_json} &amp;lt;- Jason.decode(client_data_json_str),
         {:ok, challenge} &amp;lt;- check_client_data_json(client_data_json),
         true &amp;lt;- challenge == get_session(conn, :webauthn_challenge),
         true &amp;lt;- :binary.part(authenticator_data, 0, 32) == :crypto.hash(:sha256, &amp;quot;localhost&amp;quot;),
         true &amp;lt;- (:binary.at(authenticator_data, 32) &amp;amp;&amp;amp;&amp;amp; 1) == 1 do
      conn
      |&amp;gt; delete_session(:webauthn_challenge)
      |&amp;gt; UserAuth.log_in_user_without_redirect(credential.user)
      |&amp;gt; json(%{status: :ok})
    else
      _ -&amp;gt;
        json(conn, %{status: :error})
    end
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here’s everything that we’re doing:&lt;/p&gt;&lt;ol&gt;
&lt;li&gt;Lookup the credential with the given ID.&lt;/li&gt;
&lt;li&gt;Use the public key we had stored to verify the signature on the authenticator data and client data JSON.&lt;/li&gt;
&lt;li&gt;Decode the client data JSON.&lt;/li&gt;
&lt;li&gt;Check that all the values in the client data are what we expect, and extract the challenge that was signed.&lt;/li&gt;
&lt;li&gt;Ensure the challenge that the user signed matches what we previously generated.&lt;/li&gt;
&lt;li&gt;Extract the hash of the origin from the &lt;a href=&quot;https://w3c.github.io/webauthn/#authenticator-data&quot;&gt;authenticator data&lt;/a&gt; and ensure it matches our origin (this would not be localhost in production).&lt;/li&gt;
&lt;li&gt;Check that the authenticator data has the user presence bit set (indicating that a person was actually present on the client’s end).&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;If all of those steps succeeded, we remove the old challenge value from the session (since it’s no longer needed), actually log the user in, and then respond with the “ok” status that the JavaScript is expecting. If any step failed, we’ll respond with the “error” status and the frontend will alert the user.&lt;/p&gt;&lt;p&gt;Since there’s a lot going on here, it’s worth being clear about what exactly in this process lets us authenticate and prove that the user is who they claim to be. Since the signature verification step is using the public key that we stored during the registration process, we know that anyone that can produce a valid signature using that public key must be the user (or at any rate, have their private key). The value that they’re signing is, essentially, the challenge: a securely generated random value. The user isn’t directly signing the challenge, but this is still safe, since the challenge value is included in the client data JSON, that hash of which is included in the signed message.&lt;/p&gt;&lt;p&gt;So: the challenge value that was signed by the user must be in the client data, and the challenge value in the client data must be the one we generated. Given that, we know that the user whose key was used to sign the message is the one trying to log in now. That we’re verifying with the stored public key prevents an attacker from using an arbitrary key to sign the login attempt. And that the signed challenge matches the challenge the server generated means an attacker can’t reuse a previous response to login (a replay attack).&lt;/p&gt;&lt;p&gt;At long last, we finally have the ability to log in to our application using a passkey. Only a few minor things to go, so let’s forge ahead.&lt;/p&gt;&lt;h2&gt;Handling login if the user enters an email §&lt;/h2&gt;&lt;p&gt;Although we’re presenting the conditional UI, there’s nothing preventing the user from typing their email into the field and then clicking “Sign in,” so we should probably handle that to. This can be done fairly simply by reusing our existing code for conditional login.&lt;/p&gt;&lt;p&gt;We’ll change the &lt;code&gt;loginWebAuthnAccount&lt;/code&gt; to take an additional parameter, &lt;code&gt;conditional&lt;/code&gt;, which will be a boolean indicating whether this login attempt is to setup the conditional UI or triggered by submitting the login form.&lt;/p&gt;&lt;p&gt;If it’s false, we won’t request conditional mediation and instead we’ll look up the credentials corresponding to the email the user entered and ask WebAuthn for one of those:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;async function loginWebAuthnAccount(loginForm, conditional) {
    // ...
    let allowCredentials = [];
    if (!conditional) {
		const email = loginForm[&amp;quot;user[email]&amp;quot;].value;
		const resp = await fetch(`/users/log_in/credentials?email=${email}`);
		const respJSON = await resp.json();
		allowCredentials = respJSON.map((id) =&amp;gt; {
			return {
				type: &amp;quot;public-key&amp;quot;,
				id: base64ToArrayBuffer(id),
			};
		});
    }

    const getOptions = {
		mediation: conditional ? &amp;quot;conditional&amp;quot; : &amp;quot;optional&amp;quot;,
		publicKey: {
			challenge: base64URLToArrayBuffer(challenge),
			rpId: &amp;quot;localhost&amp;quot;,
			allowCredentials,
		}
    };
    // ...
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The “optional” value for the &lt;code&gt;mediation&lt;/code&gt; option means that the authenticator isn’t required to display UI, but will do so if its policies dictate that. The &lt;code&gt;allowCredentials&lt;/code&gt; array contains objects describing all of the credentials that we want to accept—specifically, their binary IDs as &lt;code&gt;ArrayBuffer&lt;/code&gt;s.&lt;/p&gt;&lt;p&gt;We look up the user’s credentials so that the one we actually request from the authenticator matches the account that the user is trying to log in with. To handle this, we’ll also wire up an additional route on the backend that returns the base 64-encoded IDs of all the credentials belonging to the user with a given email.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;get_credentials_by_email&lt;/code&gt; function is quite simple. It just looks up a user by email, preloading any credentials they have and then returning them:&lt;/p&gt;&lt;p&gt;Back in the JS, we can tweak the setup code to pass &lt;code&gt;true&lt;/code&gt; for the conditional parameter in the initial request and also register a submit handler on the login form that will invoke it with &lt;code&gt;false&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;document.addEventListener(&amp;quot;DOMContentLoaded&amp;quot;, () =&amp;gt; {
    // ...
	const loginForm = document.getElementById(&amp;quot;login-form&amp;quot;);
	if (loginForm) {
		loginWebAuthnAccount(loginForm, true);
		loginForm.addEventListener(&amp;quot;submit&amp;quot;, (event) =&amp;gt; {
			event.preventDefault();
			loginWebAuthnAccount(loginForm, false);
		});
	}
});&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And so we’ve handled the case where the user ignores the conditional UI and still types in their email to log in.&lt;/p&gt;&lt;h2&gt;Passkey reset §&lt;/h2&gt;&lt;p&gt;A not-infrequent argument against passkeys is that if your phone falls into a lake, you lose access to all of your passkey-backed accounts. One could argue that this isn’t true because the definition of passkeys means that they’re backed up and thus protected from this sort of event (and indeed, earlier we only permitted registering with backed-up credentials). I think the argument isn’t particularly interesting, however, because you can still have a “Forgot my passkey” option that works just like it does now with passwords.&lt;/p&gt;&lt;p&gt;This is less secure than a passkey implementation that has no “Forgot” option. But it’s no less secure than current password-based systems, and I think the UX/security tradeoff here falls on the UX side—people will, inevitably, lose access to their passkeys while retaining access to their email.&lt;/p&gt;&lt;p&gt;Implementing isn’t too complicated, fortunately, since we can reuse much of the registration code. First, the JavaScript. The only change necessary is attaching the registration function to the reset form as well.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;document.addEventListener(&amp;quot;DOMContentLoaded&amp;quot;, () =&amp;gt; {
    // ...
	const resetForm = document.getElementById(&amp;quot;reset-passkey-form&amp;quot;);
	if (resetForm) {
		resetForm.addEventListener(&amp;quot;submit&amp;quot;, (event) =&amp;gt; {
			event.preventDefault();
			registerWebAuthnAccount(resetForm);
		});
	}
});&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the HTML for the reset form, we we need to include the email in the same form field as the registration form (and also add an ID to the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element):&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&amp;lt;input type=&amp;quot;hidden&amp;quot; name=&amp;quot;user[email]&amp;quot; value={@user.email} /&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the function for the reset route (which is changed to POST from PUT, to match the signup route), we take the credential ID and public key and use them to update the user’s credentials:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeysWeb.UserResetPasswordController do
  def update(conn, %{
        &amp;quot;credential_id&amp;quot; =&amp;gt; credential_id,
        &amp;quot;public_key_spki&amp;quot; =&amp;gt; public_key_spki
      }) do
    credential_id = Base.decode64!(credential_id)
    public_key_spki = Base.decode64!(public_key_spki)
    case Accounts.reset_user_credentials(conn.assigns.user, credential_id, public_key_spki) do
      :ok -&amp;gt;
        conn
        |&amp;gt; put_flash(:info, &amp;quot;Passkey reset successfully.&amp;quot;)
        |&amp;gt; UserAuth.log_in_user_without_redirect(conn.assigns.user)
        |&amp;gt; json(%{status: :ok})

      {:error, _} -&amp;gt;
        conn
        |&amp;gt; put_flash(:error, &amp;quot;Error resetting passkey&amp;quot;)
        |&amp;gt; json(%{status: :error})
    end
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After updating, we log the user in if applicable and return JSON with the appropriate status.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;reset_user_credentials&lt;/code&gt; function works very similarly to the original reset password function that was part of the template: it deletes all the user’s existing sessions, and then removes their existing credentials and creates a new one:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;defmodule PhoenixPasskeys.Accounts do
  def reset_user_credentials(user, credential_id, public_key_spki) do
    Ecto.Multi.new()
    |&amp;gt; Ecto.Multi.delete_all(
      :old_credentials,
      from(a in UserCredential, where: a.user_id == ^user.id)
    )
    |&amp;gt; Ecto.Multi.insert(
      :new_credential,
      UserCredential.changeset(%UserCredential{}, %{
        id: credential_id,
        public_key_spki: public_key_spki,
        user_id: user.id
      })
    )
    |&amp;gt; Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all))
    |&amp;gt; Repo.transaction()
    |&amp;gt; case do
      {:ok, _} -&amp;gt; :ok
      {:error, _, changeset, _} -&amp;gt; {:error, changeset}
    end
  end
end&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It’s worth noting that this does have slightly weaker security properties than the &lt;code&gt;phx.gen.auth&lt;/code&gt; reset password implementation. With that approach, leaking a password reset token does not necessarily result in an account takeover, since whoever obtained the leaked token may not know the target user’s email address. Since the auth template forces the user to re-login after a reset, this prevents someone without the email address from gaining access even if they change the password.&lt;/p&gt;&lt;p&gt;But since logging in with a passkey is functionally a single factor, resetting it means gaining access to the account. So, a leaked reset token gives the bearer control over the account. This is an argument against having a reset option, but whether this is a concern in practice depends on your specific circumstances.&lt;/p&gt;&lt;h2&gt;Conclusion §&lt;/h2&gt;&lt;p&gt;As noted, this is not a complete implementation. There are a handful of places where I’ve left things unfinished since this isn’t meant to be production-level code. There are also a few places where there are security decisions that need to be made on a more contextual basis, that I’ve tried to note. And, of course, you wouldn’t really want to only permit signing in with passkeys and wholesale drop the passwords column from your database.&lt;/p&gt;&lt;p&gt;Nonetheless, I hope this has been a helpful look at how to implement passkeys in an Elixir/Phoenix application. You can find the complete repo &lt;a href=&quot;https://git.shadowfacts.net/shadowfacts/phoenix_passkeys&quot;&gt;here&lt;/a&gt;, and it may also be useful to look at the &lt;a href=&quot;https://git.shadowfacts.net/shadowfacts/phoenix_passkeys/commit/ce4f485dbc4e528bdd13a57b0d379012dd893338&quot;&gt;specific commit&lt;/a&gt; where passkey support was added.&lt;/p&gt;</content:encoded>
</item>
<item>
<title>From legacy code to verifiable specifications with Surveyor</title>
<link>https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor</link>
<guid isPermaLink="false">OwKRRarDBvCxLjw8oGuHThV0mW_dK7NcEu5NrA==</guid>
<pubDate>Wed, 29 Apr 2026 20:10:39 +0000</pubDate>
<description>Take ownership of code you don&#39;t trust. Surveyor extracts a verifiable behavioral specification of a legacy system. Rewrite with certainty.</description>
<content:encoded>&lt;h2&gt;What is legacy code?&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#what-is-legacy-code&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Sometimes that&amp;#39;s the fifteen-year-old monolith.
Sometimes it&amp;#39;s the service that grew complicated faster than the team could keep up with.
Sometimes it&amp;#39;s that project that started as a shortcut and turned into a roadblock. Legacy code has been exposed to real world demands long enough to gather fixes for edge cases we are not aware of anymore.&lt;/p&gt;&lt;p&gt;And — increasingly — sometimes it&amp;#39;s the repo your CEO vibe-coded last weekend and that they proudly present on Monday.
It runs. Now you have to own it.&lt;/p&gt;&lt;h2&gt;Legacy software is the code we lost confidence in&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#legacy-software-is-the-code-we-lost-confidence-in&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;That last category is genuinely new and growing fast. Code written once, never reviewed, tested a bit, deployed one day. Or, worse, released as a library and never maintained after that.&lt;/p&gt;&lt;p&gt;A new kind of technical dept has evolved: &lt;strong&gt;&lt;em&gt;&amp;quot;The code we should review thoroughly one day&lt;/em&gt;&lt;/strong&gt;.&amp;quot;&lt;/p&gt;&lt;h2&gt;&amp;quot;Can we rewrite this?&amp;quot;&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#can-we-rewrite-this&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;The actual problem is not the writing of the new code, but the discovery of what the old code does, and the certainty that the new one does the same thing.&lt;/p&gt;&lt;p&gt;We&amp;#39;ve been building two small Elixir tools to help with both halves of that problem.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Surveyor&lt;/strong&gt; scans a codebase in any language and produces an architectural model.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Assay&lt;/strong&gt; runs the same plain-text behavioral specs against both the legacy and the rewrite, so you can prove they behave the same.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Together they cover the workflow: Figure out the structure, capture the behavior, rewrite, prove the rewrite works.&lt;/p&gt;&lt;div&gt;&lt;div&gt;TL;DR&lt;/div&gt;&lt;div&gt;&lt;p&gt;Surveyor produces an &lt;strong&gt;automatically verifiable behavioral specification&lt;/strong&gt; of a legacy system.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;h2&gt;How we got here&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#how-we-got-here&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Before going into either tool, it&amp;#39;s worth walking through the shape of the problem.
The picture builds up step by step, and the final diagram only makes sense once you&amp;#39;ve seen the holes in the earlier ones.&lt;/p&gt;&lt;h3&gt;What we don&amp;#39;t want&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#what-we-dont-want&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;figure&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/step-0-158d6135059c381671508607696828b4.png&quot; alt=&quot;Legacy Codebase with a dashed arrow pointing directly at a Target Codebase, with no intermediate steps&quot; title=&quot;&quot;/&gt;&lt;figcaption&gt;A direct rewrite also copies bugs, dead code and unused features.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;Point an agent at a legacy codebase, ask it to produce a new one, hope for the best.
Call it &amp;quot;the Claude zombie rewrite.&amp;quot;
You end up with a target codebase that nobody understands either, plus a generation gap between the old assumptions and the new. What has the agent overlooked, misunderstood, assumed?
A black box gets replaced with a different black box.
That is not a rewrite, that is a transcoding.&lt;/p&gt;&lt;h3&gt;What we want instead&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#what-we-want-instead&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;figure&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/step-1-b16e88b3f686956678092c1d93021641.png&quot; alt=&quot;Legacy Codebase with an arrow into a TXT file labelled &amp;amp;quot;Readable Specification&amp;amp;quot;.&quot; title=&quot;&quot;/&gt;&lt;figcaption&gt;The features of the legacy codebase need to be revised&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;The valuable artifact in the middle is not new code — it&amp;#39;s a
&lt;strong&gt;readable specification&lt;/strong&gt; of what the old code &lt;strong&gt;actually does&lt;/strong&gt;. Plain text,
human-reviewable, version-controllable. The kind of thing you need when you
want to ask the product owner if a feature is &lt;strong&gt;really&lt;/strong&gt; wanted that way. - Or if it is just a legacy quirk.&lt;/p&gt;&lt;p&gt;If you have it, you can use it for estimates. It can be your map to plan the
implementation and might even give you a hint about that stakeholder that you
might have forgotten about otherwise.&lt;/p&gt;&lt;h3&gt;Organised and illustrated&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#organised-and-illustrated&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;figure&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/step-2-9f42ae2f41b319470f65ad9ec5928524.png&quot; alt=&quot;Legacy Codebase pointing into an Architecture Map of Module A, Module B, Module C, each with its own .md spec.&quot; title=&quot;&quot;/&gt;&lt;figcaption&gt;The specification&amp;#39;s organisation should mirror the codebase&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;A single big text file describing a whole legacy system is only useful for very small projects. You want the spec broken
down by module — by bounded context — and the modules themselves arranged on a map of the architecture. Once each module
has its own spec associated to it, two things become possible: you can divide the work, and you can reason about
coverage.&lt;/p&gt;&lt;h3&gt;Job done! Or is it?&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#job-done-or-is-it&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;We now have and organised map of the system and a set of specifications. We have an overview, and the details. We could
now talk to the stakeholders, the development team, and set to work.&lt;/p&gt;&lt;figure&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/step-3-f8cc173a463a67d9b7f04b22acfa704a.png&quot; alt=&quot;A graphic showing rectangles representing the software parts map to rectangles representing the specification&quot; title=&quot;&quot;/&gt;&lt;figcaption&gt;The specification&amp;#39;s organisation should mirror the codebase&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;However, we have not yet verified our findings.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;What we are looking at is not yet a specification of the legacy system — it is a &lt;em&gt;speculation&lt;/em&gt; of the legacy system.
A document that &lt;strong&gt;claims&lt;/strong&gt; to describe what the code does, written by reading the code (or by asking an LLM to read the code).
Plausible, well-organised, reviewable. But unverified.&lt;/p&gt;&lt;h4&gt;1. Verifiability&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#1-verifiability&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;This is &lt;strong&gt;speculation as specification&lt;/strong&gt;, and it&amp;#39;s the trap most &amp;quot;let&amp;#39;s document the legacy system&amp;quot; projects fall into.
A Word document of &amp;quot;what the system does&amp;quot; can be worse than no document at all, because people start trusting it.
But even if the specification is done as a group effort by all stakeholders involved, and is thoroughly verified, it
still lacks one critical property:&lt;/p&gt;&lt;h4&gt;2. Reproducibility&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#2-reproducibility&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;What we actually want is a &lt;strong&gt;verifiable specification&lt;/strong&gt;: Not &amp;quot;verified once,&amp;quot; but verifiable on demand, any time someone
asks the question. You could verify a paper spec by hand, of course — sit down, read it, run the system, tick off the
scenarios — but manual verification is expensive enough that it gets done once at sign-off and never again.&lt;/p&gt;&lt;p&gt;A verification you don&amp;#39;t actually do is verification that doesn&amp;#39;t exist. And unless the legacy codebase is a museum
exhibit, it&amp;#39;s a moving target:&lt;/p&gt;&lt;p&gt;The team is still shipping fixes, the system is still drifting, and a paper spec written on Monday is no longer accurate
by Friday.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/step-6-ff8b98aeca5f9fdf5732fe355a44fbc0.png&quot; alt=&quot;Same picture but with a red horse galloping across the bottom and the words &amp;amp;quot;BUT SPECS GO STALE&amp;amp;quot;. The Legacy Codebase is now annotated as &amp;amp;quot;A moving target&amp;amp;quot;.&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;h3&gt;Runnable specs&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#runnable-specs&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;The fix is to make the specs &lt;strong&gt;executable&lt;/strong&gt; — to wire them into something that can run them against the actual legacy system, on demand, and tell you whether they still hold.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/step-7-444bd86f1323e81fa52511f1d57830f1.png&quot; alt=&quot;The earlier picture with a new &amp;amp;quot;SOME MAGIC&amp;amp;quot; box added below, with arrows looping the .assay specs back through the magic box and into the legacy codebase.&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;That is the move from speculation to verifiable specification: not a one-shot audit, but a button you can press on every commit, every nightly, every time anyone asks &amp;quot;is this still true?&amp;quot;&lt;/p&gt;&lt;p&gt;A &lt;strong&gt;green&lt;/strong&gt; run proves the spec is still true. A &lt;strong&gt;red&lt;/strong&gt; run is a useful signal: either the legacy drifted, or your spec was wrong.
Either way, you find out before the rewrite, not during it.&lt;/p&gt;&lt;h3&gt;That magic is Assay&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#that-magic-is-assay&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/step-8-c06ddef9a1740cfb355acb10dace6184.png&quot; alt=&quot;The bottom box is now labelled &amp;amp;quot;Assay — A framework to automate assertions&amp;amp;quot; with a Parser that reads .assay files and a &amp;amp;quot;(The parts you fill in)&amp;amp;quot; placeholder beneath it.&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Assay is the runner.
It parses the &lt;code&gt;.assay&lt;/code&gt; specs Surveyor produced, matches each step against bindings you write, and exercises the real legacy system through whatever interface it actually exposes — HTTP, CLI, message queue, file drop, whatever.
The framework is small.&lt;/p&gt;&lt;p&gt;The runtime contract is &amp;quot;your bindings + your assertions + a deterministic runner.&amp;quot;&lt;/p&gt;&lt;h3&gt;The systemic approach&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#the-systemic-approach&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/overview-110149d565e655cbe254faa9456037a1.png&quot; alt=&quot;The full picture: Surveyor on top with Architecture Map and .assay specs; Assay below with Parser, Pattern Recognition, Bindings, and Given/When/Then automations; arrows looping back to the Legacy Codebase.&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Open up that &amp;quot;parts you fill in&amp;quot; box and you find the actual surface area of work: pattern recognition for step matching, a binding per target system, and Given/When/Then automations for setup, action, and verification.&lt;/p&gt;&lt;p&gt;The end state has a property that nothing in the earlier pictures had: the problem space has been &lt;strong&gt;dissected into manageable pieces&lt;/strong&gt;.
Architecture, behavior, target adapters, assertions — each is small, each can be reviewed independently, and each can be picked up by a human or an agent without having to hold the whole system in their head at once.&lt;/p&gt;&lt;p&gt;That&amp;#39;s the goal of the toolkit.
Everything below is the detail of how each piece is built.&lt;/p&gt;&lt;h2&gt;Surveyor — discovering the architecture you inherited&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#surveyor--discovering-the-architecture-you-inherited&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Surveyor is a Mix project that takes a path to a codebase and produces a &lt;a href=&quot;https://structurizr.com/&quot;&gt;Structurizr&lt;/a&gt; DSL workspace file.
It runs in three phases mapping to the C4 model: C1 (system context), C2 (containers), C3 (components per container).&lt;/p&gt;&lt;p&gt;Crucially, Surveyor does &lt;strong&gt;not&lt;/strong&gt; parse code.
There are no language-specific parsers, no AST walkers, no fragile grammar files for thirty different ecosystems.&lt;/p&gt;&lt;p&gt;It scans the filesystem to identify what&amp;#39;s there — languages, frameworks, project files, entry points, deployment manifests — and then feeds meaningful chunks to an LLM. That sounds trivial, but is the same mechanism that commercial coding agents use when
they analyse your code.&lt;/p&gt;&lt;p&gt;Surveyor&amp;#39;s intelligence is in the prompts and the chunking, not in a stack of half-broken language adapters.&lt;/p&gt;&lt;p&gt;The CLI is interactive at every phase.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;$ surveyor ./legacy-monolith --phase c1

Phase 1 — System Context
  Scanning codebase...
  Querying LLM...

  System: &amp;quot;Order Management System&amp;quot;
  Description: Manages the full order lifecycle

  Actors:
    ✓ Customer — places and tracks orders via Web UI
    ✓ Warehouse Staff — manages fulfillment via Back Office
    ? Admin — found references in auth config (confidence: low)

  External Systems:
    ✓ Stripe — payment processing (REST API)
    ? SendGrid — found API key in config (confidence: low)

[a]ccept  [e]dit  [r]etry with more context  [q]uit
&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The LLM is asked to flag uncertainty.
Anything tagged &lt;code&gt;confidence: low&lt;/code&gt; shows up with a &lt;code&gt;?&lt;/code&gt; and a short reasoning string for human review.
That&amp;#39;s not just nice ergonomics — it&amp;#39;s the bit that makes the tool trustworthy.
An LLM that confidently invents a &lt;code&gt;PaymentReconciliation&lt;/code&gt; container that doesn&amp;#39;t exist is worse than no model at all.
An LLM that says &amp;quot;I see a &lt;code&gt;SENDGRID_API_KEY&lt;/code&gt; in &lt;code&gt;.env.example&lt;/code&gt; but no Sendgrid client in the source, please confirm&amp;quot; is doing the right kind of work.&lt;/p&gt;&lt;p&gt;Each phase is resumable.
Results are saved as JSON in &lt;code&gt;./surveyor/&lt;/code&gt;, so a thirty-container system doesn&amp;#39;t have to finish in one sitting.
You can hop in, accept C1, take a break, come back tomorrow, and resume at C2.&lt;/p&gt;&lt;p&gt;The end product is a &lt;code&gt;workspace.dsl&lt;/code&gt; you can render in Structurizr.
But more importantly, it&amp;#39;s a &lt;code&gt;workspace.dsl&lt;/code&gt; whose components are decorated with &lt;code&gt;assay.specs&lt;/code&gt; and &lt;code&gt;assay.schema&lt;/code&gt; properties, pointing at the behavioral specs and the domain schemas for each bounded context.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;orderLifecycle = component &amp;quot;Order Lifecycle&amp;quot; &amp;quot;Orders&amp;quot; &amp;quot;Bounded Context&amp;quot; {
    properties {
        &amp;quot;assay.specs&amp;quot; &amp;quot;specs/order-lifecycle&amp;quot;
        &amp;quot;assay.schema&amp;quot; &amp;quot;schemas/order_lifecycle.ex&amp;quot;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;DDD pattern annotations from the LLM (&lt;code&gt;customer-supplier&lt;/code&gt;, &lt;code&gt;anticorruption-layer&lt;/code&gt;, …) ride along as properties on the relationship.
Every component gets &lt;code&gt;assay.specs&lt;/code&gt; and &lt;code&gt;assay.schema&lt;/code&gt; properties — the contract the next tool reads from.&lt;/p&gt;&lt;h3&gt;Examples&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#examples&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;h4&gt;The System Context of a Medical Application&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#the-system-context-of-a-medical-application&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Surveyor identified the actors and the external systems of the application. Without knowing the code, we would already
know which actor types there are&lt;/p&gt;&lt;figure&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/MeditechsystemContext-b7cb1953223f982d281b4b7110677683.png&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;&lt;figcaption&gt;The System Context of a medical application&lt;/figcaption&gt;&lt;/figure&gt;&lt;h4&gt;An example of an assay spec created in the second phase:&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#an-example-of-an-assay-spec-created-in-the-second-phase&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;figure&gt;&lt;img src=&quot;https://bitcrowd.dev/assets/images/MeditechAssay-c4-46cf27120239ca00a3e2a7202cf32936.png&quot; alt=&quot;&quot; title=&quot;&quot;/&gt;&lt;figcaption&gt;The Account Creation Specs of a medical application&lt;/figcaption&gt;&lt;/figure&gt;&lt;h2&gt;Assay — proving the rewrite behaves the same&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#assay--proving-the-rewrite-behaves-the-same&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Assay is a minimal behavioral spec runner.
Specs are plain text and look like this:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;Component: [orderLifecycle] Order Lifecycle
Context: Order Placement

Definitions:
  - &amp;quot;a valid customer&amp;quot; means:
      a Customer with status Active, verified email,
      and a credit limit greater than zero

Invariants:
  - total must not exceed customer credit limit
  - at least one line item required

Rule: Customers can place orders for in-stock items

  @critical @phase-1
  Scenario: [OL-001] Place a simple order
    Given a valid customer &amp;quot;Alice&amp;quot; with credit limit €10,000
    And product &amp;quot;Widget&amp;quot; is in stock with 50 units available
    When Alice places an order for 3 units of &amp;quot;Widget&amp;quot;
    Then the order status becomes &amp;quot;Placed&amp;quot;
    And stock for &amp;quot;Widget&amp;quot; is reduced to 47 units&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The bracketed &lt;code&gt;[orderLifecycle]&lt;/code&gt; is a workspace.dsl identifier — the same identifier Surveyor wrote into the architecture model.
The bracketed &lt;code&gt;[OL-001]&lt;/code&gt; on the scenario is an optional free-form id that survives across rewrites of the spec text and lets you cross-reference from tickets or audit trails.&lt;/p&gt;&lt;p&gt;If that looks like Cucumber or Gherkin, it does — with one large difference.
There is no glue layer, no abstraction over what kind of system you are testing, no framework opinions about HTTP, databases, or browsers.
The bindings are just Elixir.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;defmoduleTargets.Legacy.OrderLifecycledo
useAssay.Binding,component:&amp;quot;orderLifecycle&amp;quot;

@baseSystem.get_env(&amp;quot;LEGACY_API_URL&amp;quot;)

  step :given,~r/a valid customer &amp;quot;(?P&amp;lt;name&amp;gt;.+)&amp;quot; with credit limit €(?P&amp;lt;limit&amp;gt;[\d,.]+)/do
{:ok, resp}=Req.post(&amp;quot;#{@base}/test/customers&amp;quot;,json:%{
name:params().name,
credit_limit:parse_money(params().limit),
status:&amp;quot;Active&amp;quot;
})
assign(customer_id: resp.body[&amp;quot;id&amp;quot;])
end

  step :action,~r/.+ places an order for (?P&amp;lt;qty&amp;gt;\d+) units? of &amp;quot;(?P&amp;lt;product&amp;gt;.+)&amp;quot;/do
{:ok, resp}=Req.post(&amp;quot;#{@base}/orders&amp;quot;,json:%{
customer_id:var(:customer_id),
lines:[%{product_id:var(:product_id),quantity:to_int(params().qty)}]
})
assign(order_id: resp.body[&amp;quot;id&amp;quot;],http_status: resp.status)
end

  step :expect,~r/the order status becomes &amp;quot;(?P&amp;lt;status&amp;gt;.+)&amp;quot;/do
{:ok, resp}=Req.get(&amp;quot;#{@base}/orders/#{var(:order_id)}&amp;quot;)
    assert resp.body[&amp;quot;status&amp;quot;]==params().status
end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;A binding is just a module that brings whatever it needs — &lt;code&gt;Req&lt;/code&gt; for an HTTP API, &lt;code&gt;AMQP&lt;/code&gt; for a message broker, &lt;code&gt;System.cmd&lt;/code&gt; for a batch processor, &lt;code&gt;File&lt;/code&gt; for a directory-watching pipeline.
Assay itself does not ship an HTTP client or a database adapter.&lt;/p&gt;&lt;p&gt;The framework provides parsing, step matching, scenario lifecycle, and assertions; the rest is your code.
That&amp;#39;s what makes the same spec runnable against an HTTP API, a CLI tool, a batch job, or a message queue, depending on what the legacy system happens to be.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;
🚀 Would you like to run this on your legacy codebase? Sign up for early access! &lt;a href=&quot;https://73f4313d.sibforms.com/serve/MUIFAGf7xJuMm4W5YSsztruKlELH459zdPyB50IhmpgBnS6wyrTGqz7dQ55IJLSVa7CUPFvAHrUEvEYbHEn5tBivo8SQPN_cJIMdBr_O1xiQ9ug64k46sfNBIFuTHK1rNKqvqytuDkTUoh0C5XMyUKOPZy7whNQM8zviGihyCCrZYBTq7uTf5-35fK6Ki2YBOVnEAmV_WwrgoT1-wg==&quot;&gt;Count me in!&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The crucial property is target swappability:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;assay run specs/order-lifecycle/ --target legacy
assay run specs/order-lifecycle/ --target new&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Same specs. Different bindings. Different systems.
The legacy binding hits the old SOAP API; the new binding hits the new Phoenix endpoint.
If both go green, the rewrite is behavior-equivalent for the things the spec covers — which, after a few months of writing specs, is most of the things that matter.&lt;/p&gt;&lt;p&gt;There is no separate &lt;code&gt;.exs&lt;/code&gt; file generated from the specs.
The runner parses &lt;code&gt;.assay&lt;/code&gt; files, matches each step against a regex on a binding function, executes them, and prints pass/fail.
That&amp;#39;s it.&lt;/p&gt;&lt;h3&gt;How Assay actually works&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#how-assay-actually-works&quot;&gt;​&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;The runtime is around six hundred lines of Elixir.
Five design choices do most of the work.&lt;/p&gt;&lt;h4&gt;A two-pass parser&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#a-two-pass-parser&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;The first pass lifts triple-quoted doc strings out of the file (so the line tokenizer doesn&amp;#39;t have to reason about them).
The second pass dispatches on the first keyword on each line.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;defparse_string(content)do
{filtered, doc_strings}=
    content
|&amp;gt;String.split(&amp;quot;\n&amp;quot;)
|&amp;gt;Enum.with_index(1)
|&amp;gt;extract_doc_strings()

Process.put(:assay_doc_strings, doc_strings)

  filtered
|&amp;gt;Enum.reject(fn{line, _}-&amp;gt;
    trimmed =String.trim(line)
    trimmed ==&amp;quot;&amp;quot;orString.starts_with?(trimmed,&amp;quot;#&amp;quot;)
end)
|&amp;gt;parse_lines(%Spec{})
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Output is a plain &lt;code&gt;%Spec{}&lt;/code&gt; struct. No AST nodes, no string interpolation.&lt;/p&gt;&lt;h4&gt;The step macro generates real module functions&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#the-step-macro-generates-real-module-functions&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Each &lt;code&gt;step :given, ~r/.../ do ... end&lt;/code&gt; call expands into a uniquely-named function plus a &lt;code&gt;%StepBinding{}&lt;/code&gt; record on an accumulating attribute.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;defmacrostep(type, regex,do: block)
when type in[:given,:action,:expect]do
  fn_name = :&amp;quot;__step_#{:erlang.unique_integer([:positive])}__&amp;quot;

quotedo
@assay_steps{unquote(type),unquote(regex),&amp;amp;__MODULE__.unquote(fn_name)/0}

defunquote(fn_name)()do
unquote(block)
end
end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;At compile time, &lt;code&gt;__before_compile__&lt;/code&gt; exposes &lt;code&gt;__steps__/0&lt;/code&gt; and &lt;code&gt;__component__/0&lt;/code&gt; for the runner to read.
There is no string codegen, no eval — just normal Elixir function definitions.&lt;/p&gt;&lt;h4&gt;Component-scoped step matching&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#component-scoped-step-matching&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Anyone who has used Cucumber has hit the global step-definition problem: the same step text means different things in different contexts, but Gherkin treats step definitions as one shared namespace.
Assay scopes bindings by component:&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;defprun_spec(spec, bindings, target, tags, exclude_tags)do
  component_bindings =
Enum.filter(bindings,fn b -&amp;gt; b.component == spec.component end)

# ... run each scenario against component_bindings only
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This is the direct reason Surveyor and Assay use the same identifier convention.
The architecture model and the spec runner share a vocabulary, and step text never collides across bounded contexts.&lt;/p&gt;&lt;h4&gt;Pre-check, then execute&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#pre-check-then-execute&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Before running any step, the runner walks the whole scenario and finds a binding for every step.
If anything is unbound, the scenario fails before any side effects.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;defrun_scenario(scenario, bindings)do
  matched_steps =
Enum.map(scenario.steps,fn step -&amp;gt;
casefind_binding(step, bindings)do
nil-&amp;gt;{:unbound, step}
        binding -&amp;gt;{:bound, step, binding}
end
end)

ifEnum.any?(matched_steps,&amp;amp;match?({:unbound, _},&amp;amp;1))do
%ScenarioResult{status::error,error:&amp;quot;Unbound steps found&amp;quot;}
else
execute_matched_steps(scenario, matched_steps, bindings)
end
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This is what stops a scenario from running half its Givens, hitting an unbound When, and leaving a dangling test customer in the database.&lt;/p&gt;&lt;h4&gt;Per-scenario state in the process dictionary&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#per-scenario-state-in-the-process-dictionary&quot;&gt;​&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Each scenario gets a fresh context — a tiny module that stores variables, params, doc strings, and data tables in the process dictionary.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code class=&quot;codeBlockLines_e6Vv&quot;&gt;def init do
Process.put(@vars_key,%{})
Process.put(@params_key,%{})
end

defget_var(name)do
Process.get(@vars_key,%{})|&amp;gt;Map.get(name)
end

defset_vars(keyword)do
  vars =Process.get(@vars_key,%{})
Process.put(@vars_key,Enum.into(keyword, vars))
end&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The runner can &lt;code&gt;save/0&lt;/code&gt; and &lt;code&gt;restore/1&lt;/code&gt; the snapshot, which is how Assay tests itself by running its own &lt;code&gt;.assay&lt;/code&gt; specs through itself in the same process.&lt;/p&gt;&lt;p&gt;There is no plugin system, no dependency injection, no scenario hooks beyond &lt;code&gt;cleanup&lt;/code&gt;.
Adding any of those would push complexity into the framework and out of the binding, which is the wrong direction.
The framework exists to be small enough that you can read it on a Friday afternoon and trust it on Monday.&lt;/p&gt;&lt;h2&gt;How they fit together&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#how-they-fit-together&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;The flow is straightforward and it maps onto the five phases laid out in the Assay handbook.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Discovery (Surveyor).&lt;/strong&gt; Run Surveyor against the legacy codebase. Walk through the C1/C2/C3 output interactively. Edit, retry, accept. Produces &lt;code&gt;workspace.dsl&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Behavioral extraction.&lt;/strong&gt; For each component in the model, write &lt;code&gt;.assay&lt;/code&gt; specs and an Elixir schema. The &lt;code&gt;assay.specs&lt;/code&gt; and &lt;code&gt;assay.schema&lt;/code&gt; properties on every Structurizr component tell you exactly where they go: &lt;code&gt;specs/&amp;lt;context&amp;gt;/&lt;/code&gt; and &lt;code&gt;schemas/&amp;lt;context&amp;gt;.ex&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Validation.&lt;/strong&gt; Write bindings against the legacy system in &lt;code&gt;targets/legacy/&lt;/code&gt;. Run &lt;code&gt;assay run specs/ --target legacy&lt;/code&gt;. Iterate until green. You now have a verified, executable specification of the legacy system&amp;#39;s behavior.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Stabilization.&lt;/strong&gt; Review what&amp;#39;s covered, what&amp;#39;s missing, what&amp;#39;s flagged as ambiguous. Get sign-off on scope.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Rewrite.&lt;/strong&gt; Build the new system. Write bindings against it in &lt;code&gt;targets/new/&lt;/code&gt;. Run &lt;code&gt;assay run specs/ --target new&lt;/code&gt;. When that&amp;#39;s green, the rewrite is done — at least for the surface area the specs cover.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The reason the two tools sit next to each other is that the architecture model is what gives the spec work structure.
Without a model, &amp;quot;write specs for the legacy system&amp;quot; is an open-ended task with no obvious stopping point.
With the model, every component has a &lt;code&gt;specs/&lt;/code&gt; directory and the question becomes &amp;quot;is each context&amp;#39;s behavior covered?&amp;quot;
That&amp;#39;s tractable.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;
🚀 Would you like to run this on your legacy codebase? Sign up for early access! &lt;a href=&quot;https://73f4313d.sibforms.com/serve/MUIFAGf7xJuMm4W5YSsztruKlELH459zdPyB50IhmpgBnS6wyrTGqz7dQ55IJLSVa7CUPFvAHrUEvEYbHEn5tBivo8SQPN_cJIMdBr_O1xiQ9ug64k46sfNBIFuTHK1rNKqvqytuDkTUoh0C5XMyUKOPZy7whNQM8zviGihyCCrZYBTq7uTf5-35fK6Ki2YBOVnEAmV_WwrgoT1-wg==&quot;&gt;Count me in!&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;The cross-target story is what makes the rewrite verifiable.
The same parsed &lt;code&gt;.assay&lt;/code&gt; scenarios are dispatched, step by step, through two different binding modules at two different points in the project&amp;#39;s life:&lt;/p&gt;&lt;p&gt;Same spec, two bindings, two systems.
The runner doesn&amp;#39;t know or care which target it&amp;#39;s dispatching to — that&amp;#39;s the property that makes the rewrite a measurable thing rather than a leap of faith.&lt;/p&gt;&lt;h2&gt;Four audiences, one artifact&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#four-audiences-one-artifact&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Four very different parties end up reading — or executing — the same &lt;code&gt;.assay&lt;/code&gt; file: the &lt;strong&gt;product owner&lt;/strong&gt;, the &lt;strong&gt;developer&lt;/strong&gt;, the &lt;strong&gt;coding agent&lt;/strong&gt;, and the &lt;strong&gt;test runner&lt;/strong&gt;.
The interesting observation is that they don&amp;#39;t conflict on &lt;strong&gt;content&lt;/strong&gt; — they conflict on &lt;strong&gt;form&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Product owners&lt;/strong&gt; want to know what the system does in business terms, what would break if you changed X, and where the risk lives.
They don&amp;#39;t want UML.
They want to read scenarios in their domain vocabulary, in plain language, and trust them.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Developers&lt;/strong&gt; want to know how the system is structured, where the seams are, and what invariants they mustn&amp;#39;t break.
And — crucially — they want the documentation to be wrong less often than the code is.
The moment docs lie, developers stop reading them.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Coding agents&lt;/strong&gt; want machine-parseable, unambiguous, addressable artifacts with stable identifiers.
They want to be able to say &amp;quot;the assertion at &lt;code&gt;assay://orderLifecycle/order-placement/OL-001/step-3&lt;/code&gt; failed&amp;quot; and have that mean something durable.
They want types, schemas, and graphs they can traverse.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Automated tests&lt;/strong&gt; want executable bindings — the layer Assay provides — and they want failures that point back to the spec, not just to a line of code.&lt;/p&gt;&lt;p&gt;Same facts, four presentations.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;.assay&lt;/code&gt; file gives the product owner readable scenarios in domain language.
The &lt;code&gt;workspace.dsl&lt;/code&gt; plus the schema modules give the developer the structural truth.
The bracketed identifiers (&lt;code&gt;Component: [orderLifecycle]&lt;/code&gt;, &lt;code&gt;Scenario: [OL-001]&lt;/code&gt;) give the agent its stable addresses.
The runner turns the whole thing into a regression suite.
None of these audiences is asked to read the others&amp;#39; representation; they all read the same spec, surfaced at the level of detail they need.&lt;/p&gt;&lt;h2&gt;Where the LLM lives, and where it doesn&amp;#39;t&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#where-the-llm-lives-and-where-it-doesnt&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;A reasonable question at this point: with all the recent enthusiasm for LLMs, why isn&amp;#39;t more of this LLM-driven?&lt;/p&gt;&lt;p&gt;The split is deliberate.&lt;/p&gt;&lt;p&gt;Surveyor uses an LLM because architectural discovery is genuinely a language task — you&amp;#39;re reading config files, route definitions, deployment manifests, dependency lock files, and inferring &amp;quot;this is the API, that&amp;#39;s the worker, that&amp;#39;s the read model.&amp;quot;
That&amp;#39;s exactly the kind of fuzzy, evidence-weighing job an LLM does well, especially when you tell it to flag uncertainty rather than guess.&lt;/p&gt;&lt;p&gt;Assay does &lt;strong&gt;not&lt;/strong&gt; use an LLM at runtime.
The runner is deterministic: parse the spec, match the regex, execute the binding, assert.
A behavioral spec that sometimes passes and sometimes does not, depending on which model variant happened to answer, is not a spec.&lt;/p&gt;&lt;p&gt;LLMs are very welcome on the authoring side — drafting &lt;code&gt;.assay&lt;/code&gt; files from legacy source is a great agent task, and the handbook has explicit guidance for coding agents about how to do that without inventing behavior.
But the green/red signal at the end has to come from a deterministic runner, or it is not really a signal.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;
🚀 Sign up for early access:&lt;a href=&quot;https://73f4313d.sibforms.com/serve/MUIFAGf7xJuMm4W5YSsztruKlELH459zdPyB50IhmpgBnS6wyrTGqz7dQ55IJLSVa7CUPFvAHrUEvEYbHEn5tBivo8SQPN_cJIMdBr_O1xiQ9ug64k46sfNBIFuTHK1rNKqvqytuDkTUoh0C5XMyUKOPZy7whNQM8zviGihyCCrZYBTq7uTf5-35fK6Ki2YBOVnEAmV_WwrgoT1-wg==&quot;&gt;Count me in!&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;h2&gt;How you use them on a project&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#how-you-use-them-on-a-project&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;In practice, the first day of a legacy rewrite looks something like this:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Clone the repo. Run Surveyor with &lt;code&gt;--phase c1&lt;/code&gt; to get the system in context. Discuss the actors and external systems with the client — this alone surfaces &amp;quot;wait, who is the Admin role?&amp;quot; conversations that would otherwise happen three months in.&lt;/li&gt;&lt;li&gt;Run &lt;code&gt;--phase c2&lt;/code&gt; to map containers. This is where you discover the data plane that nobody documented, the cron job running on a forgotten server, the queue that two services share. Surveyor flags &lt;code&gt;confidence: low&lt;/code&gt; items; the human resolves them.&lt;/li&gt;&lt;li&gt;Run &lt;code&gt;--phase c3&lt;/code&gt; per container. By the end you have a &lt;code&gt;workspace.dsl&lt;/code&gt; that is, for the first time in the project&amp;#39;s history, an accurate picture of what is deployed.&lt;/li&gt;&lt;li&gt;Pick the highest-risk bounded context. Read the legacy source. Draft &lt;code&gt;.assay&lt;/code&gt; specs for its behavior. Write a legacy binding. Run it. Iterate until green.&lt;/li&gt;&lt;li&gt;Repeat per context, in priority order, until coverage is good enough to start the rewrite.&lt;/li&gt;&lt;li&gt;Build the new system, one bounded context at a time, with &lt;code&gt;targets/new/&lt;/code&gt; bindings going green as each context comes online.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The thing both tools are optimized for is the same: making the work legible.
A legacy rewrite is a long project with shifting personnel and a nervous client.
Both Surveyor and Assay produce artifacts — a &lt;code&gt;workspace.dsl&lt;/code&gt; and a directory of &lt;code&gt;.assay&lt;/code&gt; files — that survive turnover, show progress, and that the client can actually read.&lt;/p&gt;&lt;h2&gt;Status and next steps&lt;a href=&quot;https://bitcrowd.dev/from-legacy-code-to-verifiable-specifications-with-surveyor#status-and-next-steps&quot;&gt;​&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Both tools are ready to be tested in real world scenarios. &lt;strong&gt;This is where we need your help&lt;/strong&gt;:&lt;/p&gt;&lt;p&gt;If you have a legacy system staring you down and a rewrite on the roadmap, &lt;a href=&quot;https://73f4313d.sibforms.com/serve/MUIFAGf7xJuMm4W5YSsztruKlELH459zdPyB50IhmpgBnS6wyrTGqz7dQ55IJLSVa7CUPFvAHrUEvEYbHEn5tBivo8SQPN_cJIMdBr_O1xiQ9ug64k46sfNBIFuTHK1rNKqvqytuDkTUoh0C5XMyUKOPZy7whNQM8zviGihyCCrZYBTq7uTf5-35fK6Ki2YBOVnEAmV_WwrgoT1-wg==&quot;&gt;get in touch&lt;/a&gt;. We would love to hear from you!&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Elixir: Introduction</title>
<link>https://priver.dev/post/elixir-introduction</link>
<guid isPermaLink="false">hs1NTOkg8A_vQRj0zfvHsjQluwDtepppWf87Bw==</guid>
<pubDate>Sun, 26 Apr 2026 14:28:15 +0000</pubDate>
<description>Elixir: Introduction</description>
<content:encoded>Elixir: Introduction</content:encoded>
</item>
<item>
<title>Head of the Agents and Assistants Department</title>
<link>https://rocket-science.ru/hacking/2026/04/23/developing-as-ai</link>
<enclosure type="image/jpeg" length="0" url="https://rocket-science.ru/img/logo/logo-orig.png"></enclosure>
<guid isPermaLink="false">WkNZM12AigiuASQEvEpBhyPVLPzlG-ZnZzS21g==</guid>
<pubDate>Thu, 23 Apr 2026 10:20:28 +0000</pubDate>
<description>A first-hand account of building the Cure programming language with AI agents as co-developers—what worked, what crashed spectacularly, and the distilled rules that emerged from the wreckage.</description>
<content:encoded>&lt;p&gt;Let me state upfront: my attitude toward AI assistants cannot be expressed as a boolean value. If you need an answer to the question posed point-blank—“New York Yankees or Boston Red Sox?”—I do not watch baseball at all; I’m a Barça fan. That said, I find AI assistants a perfectly legitimate and even liquid asset. The text below is an account of what made my work with agents pleasant and reduced their errors and rough edges to an acceptable minimum.&lt;/p&gt;&lt;p&gt;A little under a year ago I began working on &lt;a href=&quot;https://cure-lang.org&quot;&gt;Cure&lt;/a&gt;, a programming language with dependent types, finite state machines as first-class citizens, SMT verification, and other niceties, compiling to the BEAM.&lt;/p&gt;&lt;p&gt;My first approach to the apparatus ended in ignominious failure. I got tangled in my own architectural decisions, started piling on crutches wherever they fit, turned the code completely into an Italian restaurant menu, and lost heart. In a fit of idiocy I had an assistant generate the website and showed it to the public—the ideas themselves were intriguing enough, dependent types and SMT solvers on the BEAM are hardly redundant, and I harboured a quiet hope for community interest. The community correctly identified the language’s site as slop generated by a language model and received my attempt with arctic indifference. I got a great many “nothing works” responses and not a single coherent suggestion for improvement (no fault of the community’s—against the backdrop of the slop-flood of those days, my project would not have looked like Noah’s Ark even to an extremely charitable observer).&lt;/p&gt;&lt;p&gt;I stepped back, examined my creation from all angles, and was forced to admit: I had produced a monster. I saw no chance of licking it into shape, even through a global refactor. I bought a ream of paper and some pencils and started drawing, in order to understand exactly where I had gone wrong. (Spoiler: I had been so enchanted by the idea itself, and so desperate to get something to launch and run, that I had done literally everything wrong.)&lt;/p&gt;&lt;p&gt;I had no intention of giving up—and before me, full height, loomed the necessity of rewriting everything from scratch without repeating the mistakes. By that point I knew that artificial assistants could significantly accelerate the actual writing of code, so I started by erecting scaffolding around the future project. It was obvious to me then (I can now confirm that former intuition with experience) that all those prompts along the lines of “You are a genius architect with three hundred years of experience designing languages with dependent types and SMT solvers” work no better than the morning pep talk to an intern at standup: “You are a great programmer who has written three hundred million billion lines of code without a debugger.” If an assistant tasked with writing coherent code to a spec needed motivational gibberish to function, it would not be worth using under any circumstances whatsoever. Seriously, think about it: a model is asked to implement a GCD module using the Euclidean algorithm—are you really suggesting its deeply baked-in internal rules will not guide it down the correct branches of the conditional operators without first being told it has the soul of a prima ballerina and an avant-garde poet? What on earth does “You are a &lt;em&gt;senior architect&lt;/em&gt;” actually mean? Do the people who advocate this believe that without such a preamble the training pathways via the memoirs of axolotl breeders will activate instead?&lt;/p&gt;&lt;p&gt;So, instead of all those skills/agents/whatever, I started by feeding this mechanised beast the source code of all my own libraries, lovingly written by hand, with the note: “Here are examples of good code. Write like this. Not like that—do not write like that.” I know it is immodest, but it is my assistant. You are welcome to feed yours your own code.&lt;/p&gt;&lt;p&gt;Then I reclaimed the wasteland: thus were born &lt;a href=&quot;https://hexdocs.pm/metastatic&quot;&gt;Metastatic&lt;/a&gt;, implementing MetaAST for different languages across different paradigms, and &lt;a href=&quot;https://hexdocs.pm/ragex&quot;&gt;Ragex&lt;/a&gt;—a RAG built on AST rather than plain text (my hunch that AST fits into a context window far more easily and is far better structured than raw source code turned out to be correct).&lt;/p&gt;&lt;p&gt;My task was building a new language; constructing a new ecosystem from scratch was not part of the brief. So I analysed existing solutions—Rust, Go, Elm, Gleam—and chose the one I considered most mature (I never promised the project would be neutral with respect to my tastes and preferences). I simply copied the Elixir ecosystem and added to it what I had personally found lacking over the last ten years. Thanks to standing on the shoulders of giants in this regard, the model wrote almost the entire ecosystem for me; I simply told it: “Look how beautifully new project creation is handled in Elixir—do the same for Cure.” Language models are strong at translation, and Elixir is &lt;a href=&quot;https://dashbit.co/blog/why-elixir-best-language-for-ai&quot;&gt;considerably more intelligible&lt;/a&gt; to them compared to almost every other language.&lt;/p&gt;&lt;p&gt;So, before the first line of code, my backpack already held: the right AST—a language in which the assistant and I can communicate far more easily than in homespun English—a handcrafted RAG, and a clear understanding that every step must be a simple, atomic change. &lt;strong&gt;The fewer choices the assistant has to make between two paths, the cleaner the result.&lt;/strong&gt; This principle outweighs the quality of all prompts combined.&lt;/p&gt;&lt;p&gt;Next I had to solve the problem of validating the produced code. My eyes are sharp, but they occasionally miss non-obvious bad decisions in review. Thus was born &lt;a href=&quot;https://hexdocs.pm/oeditus_credo&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;oeditus_credo&lt;/code&gt;&lt;/a&gt;—a set of nearly forty additional &lt;a href=&quot;https://hexdocs.pm/credo&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;credo&lt;/code&gt;&lt;/a&gt; checks covering vulnerabilities, anti-patterns, and the like. The library also ships a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix oeditus_assistant_rules&lt;/code&gt; command, a rules generator for the soulless assistant. To those rules I also added: after each stage, verify that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix format &amp;amp;&amp;amp; mix credo --strict &amp;amp;&amp;amp; mix dialyzer &amp;amp;&amp;amp; mix test&lt;/code&gt; passes; update all documentation; add regression tests for new code; then run all regression tests and confirm they are still as green as my face the morning after a party.&lt;/p&gt;&lt;p&gt;Every reasonably significant stage also ends with creating an “example” in the &lt;a href=&quot;https://github.com/am-kantox/cure-lang/tree/main/examples&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;examples&lt;/code&gt;&lt;/a&gt; folder. Something for people to look at, and the regression tests never idle.&lt;/p&gt;&lt;p&gt;At this point I felt ready to start writing actual code. Cure has certain critical parts I wrote by hand from scratch. Every other pull request gets manual edits from me. Every bug I find through manual testing I fix by hand (the obvious ones aside). And yet I reached the desired result considerably faster than if I had written every line in Vim.&lt;/p&gt;&lt;p&gt;Over the course of working on Cure I learned to keep the number of errors—and consequently manual edits—to a minimum. A fairly substantial release, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;v0.26.0&lt;/code&gt;, required not a single correction, for example. Here is the distillate of my rules for communicating with an assistant, in case it proves useful to anyone:&lt;/p&gt;&lt;ul&gt;
  &lt;li&gt;the task must be self-contained but not too large; “first define the types for numeric, then we will write the converters” does not work&lt;/li&gt;
  &lt;li&gt;inside the task there must be no undefined ambiguities that will derail our &lt;em&gt;T9&lt;/em&gt;; there must be exactly one path to the solution&lt;/li&gt;
  &lt;li&gt;before tackling any task, demand an implementation plan and edit it until all ambiguities have vanished&lt;/li&gt;
  &lt;li&gt;the task must be roughly solved in your own head before turning to the assistant; otherwise the odds of agreeing with an incorrect solution are uncomfortably high&lt;/li&gt;
  &lt;li&gt;generated code must be comprehensible and elegant, “rewrite this nicely, I fed you three gigabytes of rules” does not work; if the solution is aesthetically repellent, there is a problem with the task formulation—close the session and start over&lt;/li&gt;
  &lt;li&gt;setting a task and going for coffee is the direct path to the infinite iterations described in the previous point; the stream of unconsciousness must be watched and any attempt to stray from the planned path killed without mercy&lt;/li&gt;
  &lt;li&gt;finally, if it seems to you that the cognitive load is decreasing and that any cook could now implement such a project—you need pharmaceutical intervention; your fingers tire less, yes, but if you could not have implemented the project from scratch in a text editor, the LLM is no help to you; it will generate something suspiciously twitching, no argument there, but the first halfway-serious complexity requiring a considered architectural decision will put a cross on the whole enterprise.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;That is my experience. Yours may differ; I can bear with you on that matter.&lt;/p&gt;&lt;p&gt;Try &lt;a href=&quot;https://cure-lang.org&quot;&gt;https://cure-lang.org&lt;/a&gt; and maybe you will like it. The site now has a &lt;a href=&quot;https://cure-lang.org/playground&quot;&gt;playground&lt;/a&gt; where you can experiment with types in real time, and an almost &lt;a href=&quot;https://cure-lang.org/repl&quot;&gt;real console&lt;/a&gt; where you can play with the REPL without installing anything locally.&lt;/p&gt;&lt;p&gt;Happy curing!&lt;/p&gt;</content:encoded>
</item>
<item>
<title>Scotty, I need warp speed in three minutes - Hauleth</title>
<link>https://hauleth.dev/post/things-about-elixir-you-probably-will-never-need/</link>
<enclosure type="image/jpeg" length="0" url="https://hauleth.dev/banner.png"></enclosure>
<guid isPermaLink="false">LgEc9iGNIbYFt5qz01nZzQVKxWy9JGcaHjxqjg==</guid>
<pubDate>Mon, 20 Apr 2026 23:02:18 +0000</pubDate>
<description>My journey into optimising Elixir codebase of Ultravisor (my fork of Supabase&#39;s Supavisor). This story is not about a goal, but ~~friends~~ optimisations we met along the way.</description>
<content:encoded>&lt;p&gt;In my last larger gig I worked on fascinating project - &lt;a href=&quot;https://github.com/supabase/supavisor&quot;&gt;Postgres connection pooler written in Elixir&lt;/a&gt;. Unfortunately, due to different circumstances, this project burned me out to the ground. However what doesn&amp;#39;t kill you &lt;del&gt;is crap not a weapon&lt;/del&gt; can become great learning experience.&lt;/p&gt;&lt;p&gt;Most of my achievements in this project were related to performance. This project contains &lt;strong&gt;very&lt;/strong&gt; tight loop in form of query handler, that needed to run hundreds of thousands times per second per user connection. That mean that this functions are &lt;em&gt;very&lt;/em&gt; sensitive to even slightest performance changes. And that was my task - to find potential improvements that can be made to make this codebase be much faster.&lt;/p&gt;&lt;p&gt;After departing from Supabase I liked the project so much (mostly as learning ground) that I have created my own fork, where unrestrained from all business side of the project I could focus purely on squeezing as much of performance as I can. This project now lives as &lt;a href=&quot;https://github.com/Ultravisor/ultravisor&quot;&gt;Ultravisor&lt;/a&gt; - it is still nowhere near being done in a way that I like, but I still go back to work on it from time to time to find potential performance improvements.&lt;/p&gt;&lt;p&gt;This is a story of things that I have done and learned during that journey.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Beware&lt;/strong&gt;: It is a retrospection, so in some places my memory may be not the best.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Here I need to provide some explanation first, about how Ultravisor works with database connections. It provides 2 modes of operation:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;session&lt;/code&gt; - where each connection from user to Ultravisor checks out one connection from Ultravisor to database. It checks out once, at the start of connection, and then holds connection until the end;&lt;/li&gt;&lt;li&gt;&lt;code&gt;transaction&lt;/code&gt; - where on connection there is nothing done. Client connects to the Ultravisor and can keep that connection indefinitely without ever bothering database. Database connection is checked out &lt;em&gt;only&lt;/em&gt; when there is some request from user and is returned to the pool as soon as that result of that query is returned and DB is ready for next one.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;While &lt;code&gt;session&lt;/code&gt; mode is quite on par with other implementations of connection pooling for Postgres, &lt;code&gt;transaction&lt;/code&gt; mode is where performance is lacking and is the main focus is put. In whole article (unless mentioned otherwise) I will speak about &lt;code&gt;transaction&lt;/code&gt; mode of Ultravisor.&lt;/p&gt;&lt;h2&gt;Lesson: Flame graphs and call tracing is essential#&lt;/h2&gt;&lt;p&gt;Pretty obvious thing, but still valuable lesson for any performance optimisation endeavour. For that the great thanks to &lt;a href=&quot;https://github.com/Stratus3D&quot;&gt;Trevor Brown&lt;/a&gt; and his awesome project &lt;a href=&quot;https://github.com/Stratus3D/eflambe&quot;&gt;eFlambè&lt;/a&gt;. This helped a lot in tracing hot points in the running code.&lt;/p&gt;&lt;p&gt;Unfortunately this project seems to be less active recently and has some missing features, like &lt;a href=&quot;https://github.com/Stratus3D/eflambe/issues/48&quot;&gt;listening for given duration instead of function calls count&lt;/a&gt;. This can be partially fixed by simply listening for count of calls to &lt;code&gt;handle_event/4&lt;/code&gt; function given times and then running &lt;code&gt;cat *.bggg&lt;/code&gt; to concatenate all files into larger trace. That has disadvantages, but at least it was workable within &lt;a href=&quot;https://speedoscope.app&quot;&gt;Speedoscope&lt;/a&gt; which I also highly recommend to anyone who needs to work on such optimisation.&lt;/p&gt;&lt;p&gt;While flame graphs are awesome, there is cost to gathering them with eFlambè - it greatly affects performance. Fortunately Erlang has some built in tools that have lesser performance impact, and the &amp;quot;most modern&amp;quot; of these is &lt;a href=&quot;https://erlang.org/doc/man/tprof.html&quot;&gt;&lt;code&gt;tprof&lt;/code&gt;&lt;/a&gt;. This tool is pretty easy to use, but is less detailed than eFlambè. But even with that limitation, it provides superb insight into stuff that has greatest impact on performance, as well as it make it easier to work on long running processes, as it work asynchronously, so you can &amp;quot;manually&amp;quot; decide how long you want to trace your process.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Summary:&lt;/strong&gt; Knowing where your bottlenecks are is essential for performance optimisations.&lt;/p&gt;&lt;h2&gt;Lesson: Doing less can improve performance#&lt;/h2&gt;&lt;p&gt;Obvious thing that need to be stated - doing nothing is faster than doing something. Extracting amount of data sent over given socket using &lt;code&gt;:inet.getstat/2&lt;/code&gt; call is fast, but not free. That involves some waiting for response from either port or process handling connection, which introduces slowdown. Two possible solutions there are:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Do not gather that metric at all - sensible, but not feasible, especially when you use that metric to charge your users.&lt;/li&gt;&lt;li&gt;Gather that data less often.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The approach I have taken there is obviously 2., and the solution is dumb simple - debouncer.&lt;/p&gt;&lt;p&gt;Debouncing is an interesting technique often used in user interfaces where you accept some event, and then for some period you ignore repeated events. The reason for that is that our interfaces may have flaws that send repeated events one after another.&lt;/p&gt;&lt;p&gt;In this case Ultravisor tries to store amount of sent data after each query, but that can get expensive for many short queries. Instead I have implemented simple per-process debouncer:&lt;/p&gt;&lt;p&gt;This stores returned data in process dictionary (per-process mutable space with quick access) and if there was no call in given time-period, then we process data again. This is safe way to do so, as &lt;code&gt;:inet.getstat/2&lt;/code&gt; will always return amount of data that socket processed since it started, so data between calls will be accounted.&lt;/p&gt;&lt;p&gt;Before:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tps = 79401.392762 (without initial connection time)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After (10ms of debouncing):&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tps = 80069.646510 (without initial connection time)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After (100ms of debouncing):&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tps = 80568.825937 (without initial connection time)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;: Doing noting is more performant than doing something. Sometimes doing nothing can be quite easy.&lt;/p&gt;&lt;h2&gt;Lesson: Telemetry is not free#&lt;/h2&gt;&lt;p&gt;When working on most projects, especially Phoenix-based, one can slap &lt;code&gt;:telemetry.execute/3&lt;/code&gt; calls everywhere and notice no performance degradation&lt;sup&gt;1&lt;/sup&gt;. Unfortunately, when you do hundreds of thousands calls a second - that is not a case.&lt;/p&gt;&lt;div&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;p&gt;For unaware readers - &lt;a href=&quot;https://github.com/beam-telemetry/telemetry&quot;&gt;Telemetry&lt;/a&gt; is Erlang event dispatching system for observability events.&lt;/p&gt;&lt;/div&gt;&lt;p&gt;In this project the metrics are exposed in Prometheus/OpenMetrics format, which means that there needs to be collection system within the application. In BEAM applications the standard way to implement that is to use ETS tables to store recorded values. Fortunately there are libraries to handle that for you, and for the longest time &amp;quot;gold standard&amp;quot; for it was &lt;code&gt;telemetry_prometheus_core&lt;/code&gt; library created by Telemetry core team.&lt;/p&gt;&lt;p&gt;While for most projects that library is performant enough (because metrics aren&amp;#39;t recorded in quite tight loops), in case of this project that was not a case. There, metrics gathering is still one of the hottest spot in the codebase, even with all improvements that have been done.&lt;/p&gt;&lt;p&gt;Excerpt from &lt;code&gt;tprof&lt;/code&gt; profile:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Function&lt;/th&gt;&lt;th&gt;Calls count&lt;/th&gt;&lt;th&gt;Per call (μs)&lt;/th&gt;&lt;th&gt;Percentage&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Peep.EventHandler.store_metrics/5&lt;/code&gt;&lt;/td&gt;&lt;td&gt;1421911&lt;/td&gt;&lt;td&gt;0.13&lt;/td&gt;&lt;td&gt;4.21%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Peep.Storage.Striped.insert_metric/5&lt;/code&gt;&lt;/td&gt;&lt;td&gt;904852&lt;/td&gt;&lt;td&gt;0.26&lt;/td&gt;&lt;td&gt;5.11%&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;This is with awesome library &lt;a href=&quot;https://github.com/rkallos/peep&quot;&gt;Peep&lt;/a&gt; by &lt;a href=&quot;https://github.com/rkallos&quot;&gt;Richard Kallos&lt;/a&gt;. When using &lt;code&gt;telemetry_prometheus_core&lt;/code&gt; it was simply the most expensive thing in whole loop. Just replacing metrics gathering library with Peep gave us about 2x bump in TPS.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Summary:&lt;/strong&gt; Telemetry handler can matter in tight loops. Fast metrics gathering isn&amp;#39;t easy.&lt;/p&gt;&lt;h2&gt;Lesson: Records instead of maps or structs#&lt;/h2&gt;&lt;p&gt;Elixir uses structs for structured data. That gives a lot nice features wrt. hot code reloads, compilation graph dependencies, and other. However, because structs are maps, there is a cost. Maps have O(log n) access time to the fields, this is how maps are constructed in memory. While smaller maps have slightly different (better in most cases) characteristics, there is strict requirement that you keep your structure with less than 31 fields&lt;sup&gt;2&lt;/sup&gt; and it still has slight memory overhead. The alternative is to use &lt;a href=&quot;https://hexdocs.pm/elixir/Record.html&quot;&gt;records&lt;/a&gt;. These have better performance characteristic (always constant) irrelevant of the amount of fields at the cost of being slightly more rigid (records are tuple based) and less convenient to use (experience may vary). Additional advantage in my opinion is that it is harder to add incorrect field by using &lt;code&gt;Map&lt;/code&gt; module.&lt;/p&gt;&lt;div&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;p&gt;Current (OTP 28) limit for small map is 32 keys, but Elixir uses one key for struct name, hence 31 fields is the limit.&lt;/p&gt;&lt;/div&gt;&lt;p&gt;Before you will run and change all structs in your system to records, just remember - most of the time the difference doesn&amp;#39;t matter - just use structures.&lt;/p&gt;&lt;p&gt;Before:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tps = 81765.266264 (without initial connection time)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tps = 82147.855889 (without initial connection time)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Summary:&lt;/strong&gt; &lt;code&gt;Record&lt;/code&gt;s are super handy when you need to squeeze each bit of performance. It doesn&amp;#39;t provide much, but these adds up.&lt;/p&gt;&lt;h2&gt;Lesson: ETS tables are super fast, but not always#&lt;/h2&gt;&lt;p&gt;&lt;a href=&quot;https://www.erlang.org/doc/apps/stdlib/ets.html&quot;&gt;ETS&lt;/a&gt; is Erlang&amp;#39;s built-in module for storing key-value data in mutable way. Like built-in Redis. This structure allows for sharing some data in a way, that is easy to access from different parts of the system. One example of system that is using ETS for storing their information is Telemetry (mentioned above).&lt;/p&gt;&lt;p&gt;While for 99% of the use cases Telemetry will be fast enough, it has some problems with tight loops. Main problem is that it will always copy data from table to the caller process. That mean that it can put high memory pressure on the process that tries to retrieve data.&lt;/p&gt;&lt;p&gt;Fortunately Erlang supports another mechanism for storing globally accessible data - &lt;code&gt;persistent_term&lt;/code&gt;. Of course, there is no such thing as &amp;quot;free lunch&amp;quot; so it has substantial disadvantage - it works poorly&lt;sup&gt;3&lt;/sup&gt; with data that changes often, as removing or changing data in a key will require walk through all processes to copy data from it to processes that may use it into process memory. However - Telemetry handlers should not change a lot, you should just set them once as soon as your system start, and then ideally they will not change ever again.&lt;/p&gt;&lt;div&gt;&lt;sup&gt;3&lt;/sup&gt;&lt;p&gt;There is slight optimisation that makes it fast in some cases (single word values, like atoms), but that is not the case there, so we can ignore that.&lt;/p&gt;&lt;/div&gt;&lt;p&gt;Before&lt;sup&gt;4&lt;/sup&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tps = 76914.004685 (without initial connection time)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tps = 78479.006634 (without initial connection time)&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;sup&gt;4&lt;/sup&gt;&lt;p&gt;If you wonder why these results are lower than in previous section, it is because test conditions are identical only per section, not cross sections. In this particular case I have ran benchmark while collecting metrics (to show difference in &lt;code&gt;persistent_term&lt;/code&gt; change) while other are ran without metrics to not pollute results.&lt;/p&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Summary:&lt;/strong&gt; &lt;code&gt;persistent_term&lt;/code&gt; is awesome and super fast, so if you know that you have some data that will probably never change and will be requested &lt;em&gt;constantly&lt;/em&gt;, then it may be good place to store that data.&lt;/p&gt;&lt;h2&gt;Lesson: Calling your &lt;code&gt;GenServer&lt;/code&gt;s is fast, but not 90k times per second fast#&lt;/h2&gt;&lt;p&gt;One of the interesting observations that I have spotted is that if there are longer running queries, ones that send more data over the network than just simple short responses, then the difference between Ultravisor and &amp;quot;state of the art&amp;quot; tools like &lt;a href=&quot;https://www.pgbouncer.org&quot;&gt;PgBouncer&lt;/a&gt; or &lt;a href=&quot;https://pgdog.dev&quot;&gt;PgDog&lt;/a&gt; (that are written in non-managed languages like C and Rust) is much smaller (obviously it is still there, but it is on par, not substantially off).&lt;/p&gt;&lt;p&gt;I needed to dig more, what can be the cause of such strange behaviour. The reason was found in place where I least expected it - checking out database connection to be used.&lt;/p&gt;&lt;p&gt;Flame graph showed that almost third of the time is spent on checking out database connections, and most of that time is spent in 2 function calls, both of them are internally &lt;code&gt;gen_statem&lt;/code&gt; calls and in both most time is spent on sleeping (aka, waiting for reply).&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://hauleth.dev/post/things-about-elixir-you-probably-will-never-need/sleep.png&quot; alt=&quot;Image showing left-heavy flamegraph of the profiled application&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;Now, this one is hard thing to optimise, as in Elixir there is no mutability (almost, we will get there). This mean that if I want some form of shared queue of processes, then I need to use separate process to keep state of the queue for us, and then do &lt;code&gt;GenServer&lt;/code&gt; calls to fetch that state. What I did in such situation? What any unreasonable Elixir developer obsessed with performance would do - NIF&lt;sup&gt;5&lt;/sup&gt;.&lt;/p&gt;&lt;div&gt;&lt;sup&gt;5&lt;/sup&gt;&lt;p&gt;I wanted to use ETS there, but for that to work it lacks function like &lt;code&gt;ets:take/2&lt;/code&gt; that would return only one element from the tables with type &lt;code&gt;bag&lt;/code&gt; or &lt;code&gt;duplicate_bag&lt;/code&gt;. Or any other form of just taking out any (possibly random) element from ETS table in atomic way.&lt;/p&gt;&lt;/div&gt;&lt;p&gt;The implementation is rather basic wrapper over &lt;a href=&quot;https://doc.rust-lang.org/1.94.0/std/collections/struct.VecDeque.html&quot;&gt;&lt;code&gt;VecDeque&lt;/code&gt;s&lt;/a&gt; that allow popping single element from that queue without any message passing. The implementation is very crude, nowhere production ready. It doesn&amp;#39;t provide any form of worker restarts or anything, but works quite well as PoC of what is possible.&lt;/p&gt;&lt;p&gt;New queue also provides a way to store additional &amp;quot;metadata&amp;quot; alongside the worker PID. This allows me to store DB connection socket next to connection process, which removes need for additional call to extract that data to pass requests directly to other DB, without copying data between processes.&lt;/p&gt;&lt;p&gt;Before:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tps = 83619.640673 (without initial connection time)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;tps = 94191.475386 (without initial connection time)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Summary:&lt;/strong&gt; Sometimes one need to get creative to get around platform limitations. This may require some pesky NIFs though.&lt;/p&gt;&lt;h2&gt;Conclusions#&lt;/h2&gt;&lt;p&gt;Optimising such project was enormous fun and I think that at the current state there is nothing extra that can be done to optimise it more without optimising generated JIT-ed native code or optimising Erlang scheduler.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://hauleth.dev/post/things-about-elixir-you-probably-will-never-need/final.png&quot; alt=&quot;Final flamegraph of application&quot; title=&quot;&quot;/&gt;&lt;/p&gt;&lt;p&gt;There are some flags, that affect performance, but as it is currently unclear why these work at all (probably it is related more to the OS scheduler rather than Erlang performance), I left them out of this article for now.&lt;/p&gt;&lt;h2&gt;Post Scriptum: Good tooling helps a lot#&lt;/h2&gt;&lt;p&gt;Just after I have started that optimisation project after leaving Supabase I started using &lt;a href=&quot;https://docs.jj-vcs.dev/latest/&quot;&gt;Jujutsu&lt;/a&gt; for version control. That one thing helped me &lt;strong&gt;a lot&lt;/strong&gt; with being able to have separate branches/PRs for each of the changes, while at the same being able to work with &lt;a href=&quot;https://steveklabnik.github.io/jujutsu-tutorial/advanced/simultaneous-edits.html&quot;&gt;mega-merge&lt;/a&gt; of them all.&lt;/p&gt;&lt;p&gt;That allows me to profile code with all other noise removed, while still exposing the changes as separate reviewable units. Without that support I would need to decipher what have already been changed and/or removed from the profile.&lt;/p&gt;&lt;p&gt;Additional feature that I heavily used there is &amp;quot;anonymous branching&amp;quot;. As when working with JJ I do not need to create new name for each branch that I want to try, it was way easier to implement one idea, then just do &lt;code&gt;jj new @-&lt;/code&gt; (which branches off at the commit that is parent of the current one) and just implement alternative idea. I used that constantly to compare ideas and reject failed concepts.&lt;/p&gt;&lt;div&gt;∎&lt;/div&gt;</content:encoded>
</item>
</channel>
</rss>
