<?xml version="1.0"?><feed xmlns="http://www.w3.org/2005/Atom"><title>Danila Poyarkov — Writing</title><id>https://dannote.net/feed.xml</id><updated>2026-10-02T00:00:00Z</updated><link href="https://dannote.net/feed.xml" rel="self"/><author><name>Danila Poyarkov</name></author><entry><title>What I’ve been building this year</title><id>https://dannote.net/writing/what-ive-been-building-this-year/</id><updated>2026-10-02T00:00:00Z</updated><published>2026-10-02T00:00:00Z</published><link href="https://dannote.net/writing/what-ive-been-building-this-year/"/><summary>An end-to-end open-source platform for startup factories, assembled one missing building block at a time.</summary><content type="html"><![CDATA[<p>Since the beginning of this year, I have released dozens of open-source projects.</p>
<p>There is a <a href="https://openpencil.dev">Figma-compatible design editor</a>, a <a href="https://github.com/elixir-volt/quickbeam">JavaScript runtime</a> for the <a href="https://en.wikipedia.org/wiki/BEAM_%28Erlang_virtual_machine%29">BEAM</a>, a <a href="https://github.com/elixir-volt/volt">frontend build tool</a>, an <a href="https://github.com/elixir-volt/npm_ex">npm client</a>, several <a href="https://github.com/elixir-vibe/reach">static analyzers</a>, <a href="https://github.com/elixir-vibe/vibe">coding-agent tools</a>, a <a href="https://github.com/elixir-vibe/quackdb">DuckDB adapter</a>, <a href="https://github.com/elixir-vibe/phoenix_replay">session replay</a>, <a href="https://github.com/elixir-vibe/host_kit">deployment tooling</a>, and <a href="/projects/">many smaller libraries</a> in between.</p>
<p>Judging by my feed, it probably looks like I wake up every few days with an unrelated idea, build it, publish it, and move on to the next one.</p>
<p>That is partly my fault. I have explained each project when I released it, promised to explain how they fit together, and never did.</p>
<div class="x-post">
<blockquote>
<p>All the Elixir libraries I’ve been publishing recently and OpenPencil are part of the same vision. The idea is much bigger than just building another design harness.</p>
<p>— Danila Poyarkov (@dan_note), <a href="https://x.com/dan_note/status/2047408509445087554">23 April 2026</a></p>
</blockquote>
</div>
<h2 id="tldr">TL;DR<a href="#tldr" aria-label="Link to heading 'TL;DR'" data-heading-content="TL;DR" class="anchor"></a></h2>
<p>I am building an end-to-end open-source platform for startup factories. By a startup factory I mean a venture studio: a small team, often one technical founder working with agents, that launches many products, keeps the ones that work, and kills the rest. The platform follows each product through its whole lifecycle, from references and design drafts through prototypes, code, deployment, and acquisition, to the evidence that decides what to build next.</p>
<p>The process starts in <a href="https://github.com/open-pencil/open-pencil">OpenPencil</a>, a design editor where the founder works with an agent the way they would stand behind a product designer: describing what is in their head, watching drafts appear, and steering until the design on the canvas matches the idea. Selected designs become working <a href="https://vuejs.org">Vue</a> components. The product then moves into an <a href="https://elixir-lang.org">Elixir</a> environment that connects coding, deployment, and operation, so a coding agent can see the whole product. If a customer gets stuck, the system can trace where they came from, replay what they saw, re-rendered from the backend state, inspect the code path, and propose a next step with evidence.</p>
<p>My ambition is an open-source platform that can eventually compete with <a href="https://www.figma.com">Figma</a>, <a href="https://lovable.dev">Lovable</a>, and <a href="https://replit.com">Replit</a> on a technical founder’s terms: a commodity design format, and the work after deployment.</p>
<p>&lt;.building_this_year_project_map /&gt;</p>
<p>The rest of this post walks through the map roughly one stage at a time.</p>
<h2 id="who-i-am">Who I am<a href="#who-i-am" aria-label="Link to heading 'Who I am'" data-heading-content="Who I am" class="anchor"></a></h2>
<p>I am a programmer, but I have never stayed in one narrow part of the stack. I have done systems programming, contributed to open source in the <a href="https://gitlab.xfce.org/xfce/garcon/-/commit/aec77533132eb324180ef771e15226a752573eae">Xfce</a> and <a href="https://github.com/dannote/socks-nginx-module">nginx</a> ecosystems, done white-hat security research through <a href="https://bughunters.google.com/profile/62602ae8-cf92-4fb0-810c-c9e284f3427e">Google Bug Hunters</a> and <a href="https://bugcrowd.com/h/dannote">Bugcrowd</a>, built web applications, and occasionally worked as a designer. I tend to move to whichever layer contains the problem.</p>
<p>I have also spent a long time working in venture studios, and I have founded and run small companies with my own money.</p>
<p>Running products on my own money made the economics impossible to separate from the product. A percentage point of conversion decides whether a campaign is profitable, and the cost of operating a product decides when to stop. That lens is why the platform described here does not end at deployment.</p>
<p>The first part of that process I tried to improve was design.</p>
<h2 id="it-started-with-figma">It started with Figma<a href="#it-started-with-figma" aria-label="Link to heading 'It started with Figma'" data-heading-content="It started with Figma" class="anchor"></a></h2>
<p>I created <a href="https://github.com/dannote/figma-use"><code>figma-use</code></a> to automate repetitive work in Figma. I wanted to inspect layers, make bulk changes, generate component structures, and export assets without clicking through the interface every time.</p>
<p>Figma’s own <a href="https://modelcontextprotocol.io">MCP</a> server at the time could only read: it exposed the canvas as something close to a node-tree dump, and the agent could not change anything.</p>
<p>Coding agents became useful when the harnesses around them, the tools that wrap a model in a loop, gave them an iterative process: inspect a repository, make a change, run the application, read the error, compare the result with what was expected, try again.</p>
<p>I wanted to give design agents the same kind of loop.</p>
<p>Good design is iterative too. A designer normally begins by collecting references, then draws several drafts, compares them, and chooses a direction. After that comes a longer loop: copy, tweak, compare, choose, and repeat. The first result is rarely the final one.</p>
<p>Most AI design tools ignore this process. They try to generate a finished screen in one shot. Even when the screenshot looks impressive, the structure underneath is often useless: unnamed nested frames, no components, no tokens, and no coherent system that another designer can continue working with.</p>
<p>With <a href="https://github.com/dannote/figma-use"><code>figma-use</code></a>, an agent could work on the actual structure. It could create and modify nodes, use components and variants, describe a screen in JSX and render it as Figma layers, inspect the resulting tree, and continue from there. A <code>.figma.tsx</code> file defines components; the first render creates the master, the rest create instances:</p>
<pre class="lumis lumis-themes dark light" style="color: light-dark(#1f2328, #e6edf3); background-color: light-dark(#ffffff, #0d1117);"><code class="language-tsx" translate="no" tabindex="0"><span class="l-line" data-line="1"><span style="color: light-dark(#cf222e, #ff7b72);">import</span> <span style="color: light-dark(#1f2328, #e6edf3);">{</span> <span style="color: light-dark(#1f2328, #e6edf3);">defineComponent</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#953800, #ffa657);">Frame</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#953800, #ffa657);">Text</span> <span style="color: light-dark(#1f2328, #e6edf3);">}</span> <span style="color: light-dark(#cf222e, #ff7b72);">from</span> <span style="color: light-dark(#0a3069, #a5d6ff);">&#39;figma-use/render&#39;</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="2"></span>
<span class="l-line" data-line="3"><span style="color: light-dark(#cf222e, #ff7b72);">const</span> <span style="color: light-dark(#953800, #ffa657);">Card</span> <span style="color: light-dark(#0550ae, #79c0ff);">=</span> <span style="color: light-dark(#6639ba, #d2a8ff);">defineComponent</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span></span>
<span class="l-line" data-line="4">  <span style="color: light-dark(#0a3069, #a5d6ff);">&#39;Card&#39;</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span></span>
<span class="l-line" data-line="5">  <span style="color: light-dark(#0550ae, #79c0ff);">&lt;</span><span style="color: light-dark(#953800, #ffa657);">Frame</span> <span style="color: light-dark(#0550ae, #79c0ff);">style</span><span style="color: light-dark(#0550ae, #79c0ff);">=</span><span style="color: light-dark(#1f2328, #e6edf3);">{{</span> <span style="color: light-dark(#0550ae, #79c0ff);">p</span><span style="color: light-dark(#1f2328, #e6edf3);">:</span> <span style="color: light-dark(#0550ae, #79c0ff);">24</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">bg</span><span style="color: light-dark(#1f2328, #e6edf3);">:</span> <span style="color: light-dark(#0a3069, #a5d6ff);">&#39;#FFF&#39;</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">rounded</span><span style="color: light-dark(#1f2328, #e6edf3);">:</span> <span style="color: light-dark(#0550ae, #79c0ff);">16</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">flex</span><span style="color: light-dark(#1f2328, #e6edf3);">:</span> <span style="color: light-dark(#0a3069, #a5d6ff);">&#39;col&#39;</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">gap</span><span style="color: light-dark(#1f2328, #e6edf3);">:</span> <span style="color: light-dark(#0550ae, #79c0ff);">12</span> <span style="color: light-dark(#1f2328, #e6edf3);">}}</span><span style="color: light-dark(#0550ae, #79c0ff);">&gt;</span></span>
<span class="l-line" data-line="6">    <span style="color: light-dark(#0550ae, #79c0ff);">&lt;</span><span style="color: light-dark(#953800, #ffa657);">Text</span> <span style="color: light-dark(#0550ae, #79c0ff);">style</span><span style="color: light-dark(#0550ae, #79c0ff);">=</span><span style="color: light-dark(#1f2328, #e6edf3);">{{</span> <span style="color: light-dark(#0550ae, #79c0ff);">size</span><span style="color: light-dark(#1f2328, #e6edf3);">:</span> <span style="color: light-dark(#0550ae, #79c0ff);">22</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">weight</span><span style="color: light-dark(#1f2328, #e6edf3);">:</span> <span style="color: light-dark(#0a3069, #a5d6ff);">&#39;bold&#39;</span> <span style="color: light-dark(#1f2328, #e6edf3);">}}</span><span style="color: light-dark(#0550ae, #79c0ff);">&gt;</span>Card / Header<span style="color: light-dark(#0550ae, #79c0ff);">&lt;/</span><span style="color: light-dark(#953800, #ffa657);">Text</span><span style="color: light-dark(#0550ae, #79c0ff);">&gt;</span></span>
<span class="l-line" data-line="7">    <span style="color: light-dark(#0550ae, #79c0ff);">&lt;</span><span style="color: light-dark(#953800, #ffa657);">Text</span> <span style="color: light-dark(#0550ae, #79c0ff);">style</span><span style="color: light-dark(#0550ae, #79c0ff);">=</span><span style="color: light-dark(#1f2328, #e6edf3);">{{</span> <span style="color: light-dark(#0550ae, #79c0ff);">size</span><span style="color: light-dark(#1f2328, #e6edf3);">:</span> <span style="color: light-dark(#0550ae, #79c0ff);">14</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">color</span><span style="color: light-dark(#1f2328, #e6edf3);">:</span> <span style="color: light-dark(#0a3069, #a5d6ff);">&#39;#555&#39;</span> <span style="color: light-dark(#1f2328, #e6edf3);">}}</span><span style="color: light-dark(#0550ae, #79c0ff);">&gt;</span>Updated 2 min ago<span style="color: light-dark(#0550ae, #79c0ff);">&lt;/</span><span style="color: light-dark(#953800, #ffa657);">Text</span><span style="color: light-dark(#0550ae, #79c0ff);">&gt;</span></span>
<span class="l-line" data-line="8">    <span style="color: light-dark(#0550ae, #79c0ff);">&lt;</span><span style="color: light-dark(#953800, #ffa657);">Frame</span> <span style="color: light-dark(#0550ae, #79c0ff);">style</span><span style="color: light-dark(#0550ae, #79c0ff);">=</span><span style="color: light-dark(#1f2328, #e6edf3);">{{</span> <span style="color: light-dark(#0550ae, #79c0ff);">p</span><span style="color: light-dark(#1f2328, #e6edf3);">:</span> <span style="color: light-dark(#0550ae, #79c0ff);">10</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">bg</span><span style="color: light-dark(#1f2328, #e6edf3);">:</span> <span style="color: light-dark(#0a3069, #a5d6ff);">&#39;#2563EB&#39;</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">rounded</span><span style="color: light-dark(#1f2328, #e6edf3);">:</span> <span style="color: light-dark(#0550ae, #79c0ff);">8</span> <span style="color: light-dark(#1f2328, #e6edf3);">}}</span><span style="color: light-dark(#0550ae, #79c0ff);">&gt;</span></span>
<span class="l-line" data-line="9">      <span style="color: light-dark(#0550ae, #79c0ff);">&lt;</span><span style="color: light-dark(#953800, #ffa657);">Text</span> <span style="color: light-dark(#0550ae, #79c0ff);">style</span><span style="color: light-dark(#0550ae, #79c0ff);">=</span><span style="color: light-dark(#1f2328, #e6edf3);">{{</span> <span style="color: light-dark(#0550ae, #79c0ff);">color</span><span style="color: light-dark(#1f2328, #e6edf3);">:</span> <span style="color: light-dark(#0a3069, #a5d6ff);">&#39;#FFF&#39;</span> <span style="color: light-dark(#1f2328, #e6edf3);">}}</span><span style="color: light-dark(#0550ae, #79c0ff);">&gt;</span>Reply<span style="color: light-dark(#0550ae, #79c0ff);">&lt;/</span><span style="color: light-dark(#953800, #ffa657);">Text</span><span style="color: light-dark(#0550ae, #79c0ff);">&gt;</span></span>
<span class="l-line" data-line="10">    <span style="color: light-dark(#0550ae, #79c0ff);">&lt;/</span><span style="color: light-dark(#953800, #ffa657);">Frame</span><span style="color: light-dark(#0550ae, #79c0ff);">&gt;</span></span>
<span class="l-line" data-line="11">  <span style="color: light-dark(#0550ae, #79c0ff);">&lt;/</span><span style="color: light-dark(#953800, #ffa657);">Frame</span><span style="color: light-dark(#0550ae, #79c0ff);">&gt;</span></span>
<span class="l-line" data-line="12"><span style="color: light-dark(#1f2328, #e6edf3);">)</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="13"></span>
<span class="l-line" data-line="14"><span style="color: light-dark(#cf222e, #ff7b72);">export</span> <span style="color: light-dark(#cf222e, #ff7b72);">default</span> <span style="color: light-dark(#1f2328, #e6edf3);">()</span> <span style="color: light-dark(#0550ae, #79c0ff);">=&gt;</span> <span style="color: light-dark(#0550ae, #79c0ff);">&lt;</span><span style="color: light-dark(#953800, #ffa657);">Card</span> <span style="color: light-dark(#0550ae, #79c0ff);">/&gt;</span></span></code></pre>
<p>I added visual diffing so it could see what changed, and design linting so it could catch structural and accessibility problems. I also wanted design files in automated pipelines: linted in CI, compared between revisions, and exported without anybody opening Figma.</p>
<p>&lt;.building_this_year_design_diff /&gt;</p>
<p>Then Figma <a href="https://github.com/dannote/figma-use/issues/6#issuecomment-3925136616">released an update</a> that blocked the debugging interface <a href="https://github.com/dannote/figma-use"><code>figma-use</code></a> relied on.</p>
<p>I found workarounds and kept the tool working, but the larger lesson was obvious. I could not build this kind of infrastructure on access that a vendor could remove at any moment. If agents were going to treat design as a real programmable medium, the editor itself had to be open.</p>
<p>That is how <a href="https://github.com/open-pencil/open-pencil">OpenPencil</a> began.</p>
<div class="x-post">
<blockquote>
<p>Figma shipped a silent patch specifically to kill figma-use — my open-source tool that did what they wouldn't: an MCP server that creates and modifies designs, JSX export, design linting. Then they scrambled to catch up with their own MCP server.</p>
<p>So I spent the weekend recreating Figma from scratch.</p>
<p>OpenPencil: reads and writes .fig files, AI chat with full design tools, P2P collaboration with zero servers, ~7 MB app. No account, no subscription.</p>
<p>Three days, one developer, MIT license.</p>
<p><a href="https://openpencil.dev">https://openpencil.dev</a></p>
<p>— Danila Poyarkov (@dan_note), <a href="https://x.com/dan_note/status/2028201388074013048">1 March 2026</a></p>
</blockquote>
</div>
<h2 id="openpencil-became-a-toolkit">OpenPencil became a toolkit<a href="#openpencil-became-a-toolkit" aria-label="Link to heading 'OpenPencil became a toolkit'" data-heading-content="OpenPencil became a toolkit" class="anchor"></a></h2>
<p><a href="https://github.com/open-pencil/open-pencil">OpenPencil</a> initially gave me an independent environment for the work I had started with <a href="https://github.com/dannote/figma-use"><code>figma-use</code></a>. It could open and write <code>.fig</code> files, render them without Figma, and let agents modify the actual document structure.</p>
<p>While refactoring the project, I realized that the editor itself should not be the only useful result. I split the monolith into packages: the file format parsers, the scene graph, the editor core, the CLI, the MCP server, and a headless Vue SDK. The <a href="https://github.com/open-pencil/open-pencil">OpenPencil</a> application is now one consumer of them.</p>
<p>The Vue SDK lets developers construct a different editor shell around the same engine. They can embed an editing surface into their product, expose a restricted editor for a particular workflow, or add design tools to an IDE without forking the <a href="https://github.com/open-pencil/open-pencil">OpenPencil</a> interface.</p>
<p>The engine does not require an editor UI at all. A script or CI job can query a <code>.fig</code> file, lint it, extract tokens, convert it, render it, compare it with another revision, or modify it through the same operations used by the application and its agents.</p>
<p>The application itself has moved quickly this year. It opens <code>.fig</code> and its own <code>.pen</code> files, imports and exports the formats designers and developers already use, from HTML and Tailwind to PPTX and Storybook, and collaborates peer to peer with no server. Agents reach it through MCP, WebMCP, ACP, and a companion runtime that runs <a href="https://github.com/badlogic/pi-mono">Pi</a> inside the app.</p>
<p>My goal is to make <code>.fig</code> a commodity. If independent software can parse, query, render, modify, and convert the format, it stops being something that can only be fully used inside Figma. It becomes input for other editors, IDEs, and automated pipelines.</p>
<p>This solves access to the design structure, but not the boundary between a design and the application eventually built from it.</p>
<h2 id="from-designs-to-real-components">From designs to real components<a href="#from-designs-to-real-components" aria-label="Link to heading 'From designs to real components'" data-heading-content="From designs to real components" class="anchor"></a></h2>
<p>The next direction I started exploring is <a href="https://github.com/dannote/vue-pencil">VuePencil</a>: something like Figma, but for Vue components.</p>
<div class="x-post">
<blockquote>
<p>Here’s a teaser for another project I’ve been working on for a while.</p>
<p>It’s the next generation of OpenPencil — a visual editor where you draw directly in code. Compose real Vue components, nest slots, bind them to composables, use headless UI primitives like Reka, and export code that’s meant to run.</p>
<p>Most attempts I’ve seen fall into two traps: inventing a JSON model for canvas items, then fighting to map it back to components — or trying to WYSIWYG-edit HTML, where the editor and app end up fighting over the same DOM, runtime, and iframe boundaries, slowly turning into a worse Chrome DevTools.</p>
<p>VuePencil starts from the VNode tree. The visual editor is just another way to edit that tree.</p>
<p><img src="images/x/dan_note-2058490589356707903.jpg" alt="A frame from the VuePencil demo video: a canvas with real Vue components selected, next to the generated code." /></p>
<p>— Danila Poyarkov (@dan_note), <a href="https://x.com/dan_note/status/2058490589356707903">24 May 2026</a></p>
</blockquote>
</div>
<p>Many visual HTML builders have an abstraction problem. The editor is already an HTML application, and then it tries to build another HTML application inside itself. The layers gradually leak into each other. The tool either supports only a restricted subset of HTML and becomes brittle, or exposes more and more browser internals until it turns into a complicated version of developer tools.</p>
<p>Vue already has a suitable abstraction for this: the VNode tree, its in-memory description of what it renders. In <a href="https://github.com/dannote/vue-pencil">VuePencil</a>, that tree is the source of truth. Editor operations change the model, Vue renders it, and the editor reads the resulting geometry to position selections and handles. It does not directly rewrite the rendered DOM.</p>
<p>This also means that the things placed on the canvas can be real components. The current prototype supports <a href="https://reka-ui.com">Reka UI</a> primitives, component parts, named slots, props, bindings, and <a href="https://vueuse.org">VueUse</a> composables. A switch on the canvas is the real component, and it works in preview mode:</p>
<pre class="lumis lumis-themes dark light" style="color: light-dark(#1f2328, #e6edf3); background-color: light-dark(#ffffff, #0d1117);"><code class="language-vue" translate="no" tabindex="0"><span class="l-line" data-line="1"><span style="color: light-dark(#116329, #7ee787);">&lt;</span><span style="color: light-dark(#116329, #7ee787);">SwitchRoot</span> <span style="color: light-dark(#0550ae, #79c0ff);">v-model</span><span style="color: light-dark(#0550ae, #79c0ff);">=</span><span style="color: light-dark(#1f2328, #e6edf3);">&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">enabled</span><span style="color: light-dark(#1f2328, #e6edf3);">&quot;</span><span style="color: light-dark(#116329, #7ee787);">&gt;</span></span>
<span class="l-line" data-line="2">  <span style="color: light-dark(#116329, #7ee787);">&lt;</span><span style="color: light-dark(#116329, #7ee787);">SwitchThumb</span> <span style="color: light-dark(#116329, #7ee787);">/&gt;</span></span>
<span class="l-line" data-line="3"><span style="color: light-dark(#116329, #7ee787);">&lt;/</span><span style="color: light-dark(#116329, #7ee787);">SwitchRoot</span><span style="color: light-dark(#116329, #7ee787);">&gt;</span></span></code></pre>
<p>The result can be serialized as a normal Vue <a href="https://vuejs.org/guide/scaling-up/sfc.html">SFC</a>. Frames and canvas positions remain editor metadata. Slots become Vue slots, capabilities become composable calls, and bindings become ordinary Vue expressions.</p>
<p>This makes prototypes much more useful than links between static screens. A founder can continue the design process with working state and interactions, then take the resulting components into the coding environment. <a href="https://github.com/dannote/vue-pencil">VuePencil</a> is still an early experiment. It is the direction I want <a href="https://github.com/open-pencil/open-pencil">OpenPencil</a> to grow in: a canvas of real components, past Figma compatibility.</p>
<h2 id="why-elixir">Why Elixir?<a href="#why-elixir" aria-label="Link to heading 'Why Elixir?'" data-heading-content="Why Elixir?" class="anchor"></a></h2>
<p>Once the prototype becomes a real application, design is only one part of the problem. You need a backend, storage, background jobs, external APIs, deployment, analytics, and a way for agents to work with all of them. The question was which language all of that should speak.</p>
<p>For mathematics there is <a href="https://lean-lang.org">Lean</a>. Every definition, theorem, and proof is written in one language and checked by one small, trusted kernel. A model trained on Lean does not spend capacity learning five notations for the same idea, and every step it takes gets a verdict. My bet is that this is why such models reason so densely.</p>
<p>&lt;.building_this_year_lean_sample /&gt;</p>
<p>I want the same for the web stack: one language in which bundling, the JavaScript runtime, storage, the backend, deployment, and operations share the same kind of API, the same runtime, and the same checks, while JavaScript, Rust, and SQL keep doing their jobs underneath. Elixir is the closest thing I found, and the rest of this post is what it took to make that true. I first wrote about this intuition in <a href="/writing/a-language-for-humans-and-models/">“A language for humans and models”</a>.</p>
<p>All of it in one session, the same one an agent works in:</p>
<pre class="lumis lumis-themes dark light" style="color: light-dark(#1f2328, #e6edf3); background-color: light-dark(#ffffff, #0d1117);"><code class="language-elixir" translate="no" tabindex="0"><span class="l-line" data-line="1"><span style="color: light-dark(#57606a, #8b949e);"># Build the frontend.</span></span>
<span class="l-line" data-line="2"><span style="color: light-dark(#1f2328, #e6edf3);">{</span><span style="color: light-dark(#0550ae, #79c0ff);">:ok</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#1f2328, #e6edf3);">build</span><span style="color: light-dark(#1f2328, #e6edf3);">}</span> <span style="color: light-dark(#0550ae, #79c0ff);">=</span> <span style="color: light-dark(#953800, #ffa657);">Volt</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#6639ba, #d2a8ff);">build</span><span style="color: light-dark(#1f2328, #e6edf3);">()</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="3"></span>
<span class="l-line" data-line="4"><span style="color: light-dark(#57606a, #8b949e);"># Run JavaScript inside the application, as a supervised process.</span></span>
<span class="l-line" data-line="5"><span style="color: light-dark(#1f2328, #e6edf3);">{</span><span style="color: light-dark(#0550ae, #79c0ff);">:ok</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#1f2328, #e6edf3);">html</span><span style="color: light-dark(#1f2328, #e6edf3);">}</span> <span style="color: light-dark(#0550ae, #79c0ff);">=</span> <span style="color: light-dark(#953800, #ffa657);">QuickBEAM</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#6639ba, #d2a8ff);">call</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0550ae, #79c0ff);">:renderer</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0a3069, #a5d6ff);">&quot;render&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#1f2328, #e6edf3);">[</span><span style="color: light-dark(#1f2328, #e6edf3);">%</span><span style="color: light-dark(#1f2328, #e6edf3);">{</span><span style="color: light-dark(#0550ae, #79c0ff);">page: </span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;home&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">}])</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="6"></span>
<span class="l-line" data-line="7"><span style="color: light-dark(#57606a, #8b949e);"># Search the code by structure.</span></span>
<span class="l-line" data-line="8"><span style="color: light-dark(#953800, #ffa657);">ExAST</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#6639ba, #d2a8ff);">search</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;lib/&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0a3069, #a5d6ff);">&quot;Repo.transaction(_)&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">inside: </span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;def _ do ... end&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="9"></span>
<span class="l-line" data-line="10"><span style="color: light-dark(#57606a, #8b949e);"># Build the dependence graph of a file.</span></span>
<span class="l-line" data-line="11"><span style="color: light-dark(#1f2328, #e6edf3);">graph</span> <span style="color: light-dark(#0550ae, #79c0ff);">=</span> <span style="color: light-dark(#953800, #ffa657);">Reach</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#6639ba, #d2a8ff);">file_to_graph</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;lib/payments.ex&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="12"></span>
<span class="l-line" data-line="13"><span style="color: light-dark(#57606a, #8b949e);"># Call a model through one path, with quotas and accounting.</span></span>
<span class="l-line" data-line="14"><span style="color: light-dark(#1f2328, #e6edf3);">{</span><span style="color: light-dark(#0550ae, #79c0ff);">:ok</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#1f2328, #e6edf3);">reply</span><span style="color: light-dark(#1f2328, #e6edf3);">}</span> <span style="color: light-dark(#0550ae, #79c0ff);">=</span> <span style="color: light-dark(#953800, #ffa657);">LLMProxy</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#6639ba, #d2a8ff);">chat</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;Summarize this incident&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">model: </span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;fast&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="15"></span>
<span class="l-line" data-line="16"><span style="color: light-dark(#57606a, #8b949e);"># Query the analytics store.</span></span>
<span class="l-line" data-line="17"><span style="color: light-dark(#953800, #ffa657);">MyApp.AnalyticsRepo</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#6639ba, #d2a8ff);">all</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#953800, #ffa657);">MyApp.Analytics</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#6639ba, #d2a8ff);">category_latency</span><span style="color: light-dark(#1f2328, #e6edf3);">())</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="18"></span>
<span class="l-line" data-line="19"><span style="color: light-dark(#57606a, #8b949e);"># Fetch what one user saw.</span></span>
<span class="l-line" data-line="20"><span style="color: light-dark(#1f2328, #e6edf3);">recording</span> <span style="color: light-dark(#0550ae, #79c0ff);">=</span> <span style="color: light-dark(#953800, #ffa657);">PhoenixReplay.Recordings</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#6639ba, #d2a8ff);">fetch!</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#1f2328, #e6edf3);">id</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="21"></span>
<span class="l-line" data-line="22"><span style="color: light-dark(#57606a, #8b949e);"># Plan and apply the host.</span></span>
<span class="l-line" data-line="23"><span style="color: light-dark(#1f2328, #e6edf3);">{</span><span style="color: light-dark(#0550ae, #79c0ff);">:ok</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#1f2328, #e6edf3);">plan</span><span style="color: light-dark(#1f2328, #e6edf3);">}</span> <span style="color: light-dark(#0550ae, #79c0ff);">=</span> <span style="color: light-dark(#953800, #ffa657);">HostKit</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#6639ba, #d2a8ff);">plan</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#1f2328, #e6edf3);">project</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">target: :prod</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span></span>
<span class="l-line" data-line="24"><span style="color: light-dark(#953800, #ffa657);">HostKit</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#6639ba, #d2a8ff);">apply</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#1f2328, #e6edf3);">plan</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">confirm: </span><span style="color: light-dark(#0550ae, #79c0ff);">true</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="25"></span>
<span class="l-line" data-line="26"><span style="color: light-dark(#57606a, #8b949e);"># Ask the runtime itself.</span></span>
<span class="l-line" data-line="27"><span style="color: light-dark(#953800, #ffa657);">Supervisor</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#6639ba, #d2a8ff);">which_children</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#953800, #ffa657);">MyApp.Supervisor</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span></span></code></pre>
<p>I was skeptical when José Valim, the creator of Elixir, published <a href="https://dashbit.co/blog/why-elixir-best-language-for-ai">“Why Elixir is the best language for AI”</a>. A benchmark result like the <a href="https://autocodebench.github.io/">AutoCodeBench</a> score he cites says little on its own. One of his reasons stuck with me, though: the ecosystem has stayed stable, so a model has not learned five generations of conflicting APIs. After a few months of building Elixir with agents, I agreed.</p>
<p>Elixir is dynamic too, so this is not typed versus untyped. But data flow is explicit, pattern matching puts contracts in the code, conventions are consistent, and the compiler and the growing type system catch more with every release.</p>
<p>In JavaScript and Python projects I see the opposite. Strict types sit on top of those languages as optional layers, while their ecosystems contain many competing generations of tools and conventions. Agents mix ESM with CommonJS, use an old framework pattern beside a new one, hand-roll something the project already has, or create another implementation because they did not find the first one. Skills and prompts can reduce this, but they do not change the language underneath.</p>
<p>Then there is <a href="https://www.erlang.org/doc/system/design_principles.html">OTP</a>, the framework of processes and supervisors that Elixir inherits from Erlang. User sessions, background jobs, external API calls, and agent runs can all be represented as isolated processes with explicit ownership and supervision. One process can fail without taking the rest of the application with it. The running system is also directly inspectable. The questions an engineer asks in a shell, an agent can ask too, one call each.</p>
<p>&lt;.building_this_year_runtime_map /&gt;</p>
<p>José made the same argument from the tooling side in <a href="https://tidewave.ai/blog/the-future-of-coding-agents-is-vertical-integration">“The future of coding agents is vertical integration”</a>: an agent works much better when it can connect source code to the browser, logs, database, and running application instead of asking the developer to translate between them.</p>
<h2 id="checking-what-agents-write">Checking what agents write<a href="#checking-what-agents-write" aria-label="Link to heading 'Checking what agents write'" data-heading-content="Checking what agents write" class="anchor"></a></h2>
<p>Lean is only half notation. The other half is the kernel, which tells the model whether each step holds. Elixir's compiler, <a href="https://www.erlang.org/doc/apps/dialyzer/dialyzer.html">Dialyzer</a>, and tests are a start, but the scale at which agents produce code created a problem none of them catch: keeping that code coherent over time.</p>
<p>Agents tend to focus on the immediate task. They often fail to notice that a similar implementation already exists elsewhere, so they create another one. The copies then evolve separately: a bug is fixed in one but not the other, their behavior gradually diverges, and later agents add another variation because they cannot tell which one is canonical. This is one of the classic ways a vibe-coded codebase turns into a mess.</p>
<p>I started turning the checks I was performing during reviews into tools.</p>
<p><a href="https://github.com/elixir-vibe/ex_ast">ExAST</a> is structural search and replacement for Elixir. <a href="https://github.com/getgrit/gritql">Grit</a> rewrites code with structural patterns and <a href="https://codeql.github.com">CodeQL</a> queries it as a database, both across many languages through a neutral layer. ExAST is Elixir-native: a pattern is ordinary Elixir code, matched against the AST the compiler itself produces. Instead of grepping source text or inventing a regular expression, an agent searches for an Elixir syntax pattern and rewrites the matching nodes.</p>
<p>The questions a reviewer asks about generated code become queries over the tree:</p>
<pre class="lumis lumis-themes dark light" style="color: light-dark(#1f2328, #e6edf3); background-color: light-dark(#ffffff, #0d1117);"><code class="language-elixir" translate="no" tabindex="0"><span class="l-line" data-line="1"><span style="color: light-dark(#cf222e, #ff7b72);">import</span> <span style="color: light-dark(#953800, #ffa657);">ExAST.Query</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="2"></span>
<span class="l-line" data-line="3"><span style="color: light-dark(#57606a, #8b949e);"># Transactions that still have debug output inside them.</span></span>
<span class="l-line" data-line="4"><span style="color: light-dark(#6639ba, #d2a8ff);">from</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;def _ do ... end&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span></span>
<span class="l-line" data-line="5"><span style="color: light-dark(#0550ae, #79c0ff);">|&gt;</span> <span style="color: light-dark(#6639ba, #d2a8ff);">where</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#6639ba, #d2a8ff);">contains</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;Repo.transaction(_)&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">))</span></span>
<span class="l-line" data-line="6"><span style="color: light-dark(#0550ae, #79c0ff);">|&gt;</span> <span style="color: light-dark(#6639ba, #d2a8ff);">where</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#6639ba, #d2a8ff);">contains</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;IO.inspect(...)&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">))</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="7"></span>
<span class="l-line" data-line="8"><span style="color: light-dark(#57606a, #8b949e);"># Handlers for the two events the interface actually sends.</span></span>
<span class="l-line" data-line="9"><span style="color: light-dark(#6639ba, #d2a8ff);">from</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;def handle_event(event, _, _) do ... end&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span></span>
<span class="l-line" data-line="10"><span style="color: light-dark(#0550ae, #79c0ff);">|&gt;</span> <span style="color: light-dark(#6639ba, #d2a8ff);">where</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0550ae, #79c0ff);">^</span><span style="color: light-dark(#1f2328, #e6edf3);">event</span> <span style="color: light-dark(#0550ae, #79c0ff);">==</span> <span style="color: light-dark(#0550ae, #79c0ff);">:click</span> <span style="color: light-dark(#cf222e, #ff7b72);">or</span> <span style="color: light-dark(#0550ae, #79c0ff);">^</span><span style="color: light-dark(#1f2328, #e6edf3);">event</span> <span style="color: light-dark(#0550ae, #79c0ff);">==</span> <span style="color: light-dark(#0550ae, #79c0ff);">:keydown</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="11"></span>
<span class="l-line" data-line="12"><span style="color: light-dark(#57606a, #8b949e);"># Comparisons that are always true.</span></span>
<span class="l-line" data-line="13"><span style="color: light-dark(#6639ba, #d2a8ff);">from</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;left == right&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span> <span style="color: light-dark(#0550ae, #79c0ff);">|&gt;</span> <span style="color: light-dark(#6639ba, #d2a8ff);">where</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0550ae, #79c0ff);">^</span><span style="color: light-dark(#1f2328, #e6edf3);">left</span> <span style="color: light-dark(#0550ae, #79c0ff);">==</span> <span style="color: light-dark(#0550ae, #79c0ff);">^</span><span style="color: light-dark(#1f2328, #e6edf3);">right</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span></span></code></pre>
<p>Rewrites are patterns on both sides, and the agent sees a plan of every replacement and conflict before anything is applied. The same engine diffs code structurally, so a function that moved is reported as a move. ExAST sees structure only, so a plan is a proposal; whether a rewrite is safe still takes judgment.</p>
<p><a href="https://github.com/elixir-vibe/ex_dna">ExDNA</a> detects duplicated code structurally and says how to fix it. It finds exact copies and copies with renamed variables by default, and near-duplicates with changed literals when asked, then proposes the extraction: a function, a macro, or a behaviour. It runs as a compiler step, a Credo check, or a language server, so the same finding reaches CI and the editor. I use it with a zero-duplication budget in most of my projects.</p>
<p><a href="https://github.com/elixir-vibe/ex_slop">ExSlop</a> is a set of Credo checks for the other patterns generated code repeats: identity passthroughs, blanket rescues, queries inside <code>Enum.map</code>, and docs that narrate the code beneath them.</p>
<p>These tools mostly see local structure. A function can look reasonable in isolation while creating a bad dependency, bypassing an architectural boundary, or allowing untrusted input to reach a database, filesystem, or shell command.</p>
<p><a href="https://github.com/elixir-vibe/reach">Reach</a> builds a program dependence graph for Elixir, Erlang, Gleam, JavaScript, and TypeScript: calls, control flow, data flow, effects, and OTP process relationships. It answers the questions that span files: what depends on this function, what a change might affect, and whether data can flow from a source to a dangerous effect. The last one is a command, here from request parameters to the database:</p>
<pre class="lumis lumis-themes dark light" style="color: light-dark(#1f2328, #e6edf3); background-color: light-dark(#ffffff, #0d1117);"><code class="language-bash" translate="no" tabindex="0"><span class="l-line" data-line="1"><span style="color: light-dark(#0550ae, #79c0ff);">mix</span> <span style="color: light-dark(#1f2328, #e6edf3);">reach.trace</span> <span style="color: light-dark(#1f2328, #e6edf3);">--from</span> <span style="color: light-dark(#1f2328, #e6edf3);">conn.params</span> <span style="color: light-dark(#1f2328, #e6edf3);">--to</span> <span style="color: light-dark(#1f2328, #e6edf3);">Repo</span></span></code></pre>
<p>The other questions have the same shape. Here they are asked of QuackDB:</p>
<pre class="lumis lumis-themes dark light" style="color: light-dark(#1f2328, #e6edf3); background-color: light-dark(#ffffff, #0d1117);"><code class="language-bash" translate="no" tabindex="0"><span class="l-line" data-line="1"><span style="color: light-dark(#1f2328, #e6edf3);">$</span> <span style="color: light-dark(#1f2328, #e6edf3);">mix</span> <span style="color: light-dark(#1f2328, #e6edf3);">reach.map</span> <span style="color: light-dark(#1f2328, #e6edf3);">--hotspots</span> <span style="color: light-dark(#1f2328, #e6edf3);">--top</span> <span style="color: light-dark(#0550ae, #79c0ff);">3</span></span>
<span class="l-line" data-line="2">  <span style="color: light-dark(#0550ae, #79c0ff);">score</span> <span style="color: light-dark(#1f2328, #e6edf3);">combines</span> <span style="color: light-dark(#1f2328, #e6edf3);">branch</span> <span style="color: light-dark(#1f2328, #e6edf3);">count</span> <span style="color: light-dark(#1f2328, #e6edf3);">with</span> <span style="color: light-dark(#1f2328, #e6edf3);">caller</span> <span style="color: light-dark(#1f2328, #e6edf3);">impact</span></span>
<span class="l-line" data-line="3">  <span style="color: light-dark(#0550ae, #79c0ff);">QuackDB.Source.literal!/1</span>  <span style="color: light-dark(#1f2328, #e6edf3);">score=96</span>  <span style="color: light-dark(#1f2328, #e6edf3);">branches=4</span>  <span style="color: light-dark(#1f2328, #e6edf3);">callers=24</span></span>
<span class="l-line" data-line="4">  <span style="color: light-dark(#0550ae, #79c0ff);">QuackDB.SQL.literal/1</span>      <span style="color: light-dark(#1f2328, #e6edf3);">score=77</span>  <span style="color: light-dark(#1f2328, #e6edf3);">branches=7</span>  <span style="color: light-dark(#1f2328, #e6edf3);">callers=11</span></span>
<span class="l-line" data-line="5">  <span style="color: light-dark(#0550ae, #79c0ff);">QuackDB.SQL.literal!/1</span>     <span style="color: light-dark(#1f2328, #e6edf3);">score=31</span>  <span style="color: light-dark(#1f2328, #e6edf3);">branches=1</span>  <span style="color: light-dark(#1f2328, #e6edf3);">callers=31</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="6"></span>
<span class="l-line" data-line="7"><span style="color: light-dark(#1f2328, #e6edf3);">$</span> <span style="color: light-dark(#1f2328, #e6edf3);">mix</span> <span style="color: light-dark(#1f2328, #e6edf3);">reach.otp</span> <span style="color: light-dark(#1f2328, #e6edf3);">--concurrency</span></span>
<span class="l-line" data-line="8">  <span style="color: light-dark(#0550ae, #79c0ff);">Tasks</span>        <span style="color: light-dark(#1f2328, #e6edf3);">async</span>      <span style="color: light-dark(#1f2328, #e6edf3);">lib/quack_db/server.ex:423</span>   <span style="color: light-dark(#0550ae, #79c0ff);">1</span> <span style="color: light-dark(#1f2328, #e6edf3);">async</span> <span style="color: light-dark(#1f2328, #e6edf3);">without</span> <span style="color: light-dark(#1f2328, #e6edf3);">matching</span> <span style="color: light-dark(#1f2328, #e6edf3);">await</span></span>
<span class="l-line" data-line="9">  <span style="color: light-dark(#0550ae, #79c0ff);">Monitors</span>     <span style="color: light-dark(#1f2328, #e6edf3);">trap_exit</span>  <span style="color: light-dark(#1f2328, #e6edf3);">lib/quack_db/server.ex:196</span></span>
<span class="l-line" data-line="10">  <span style="color: light-dark(#0550ae, #79c0ff);">Supervisors</span>             <span style="color: light-dark(#1f2328, #e6edf3);">lib/quack_db/application.ex:12</span></span></code></pre>
<p><a href="https://github.com/elixir-vibe/reach">Reach</a> also turns architecture into something agents can check. A project declares its layers and forbidden dependencies, and changes that cross them are rejected. Findings are advisory by default, and every suggested fix is labeled equivalent, conditional, or review-only, so an agent can tell a proven rewrite from a lead.</p>
<p>This site has <a href="https://github.com/dannote/dannote.net/blob/main/.reach.exs">such a policy</a>. Three layers, and the lower ones may not reach up:</p>
<pre class="lumis lumis-themes dark light" style="color: light-dark(#1f2328, #e6edf3); background-color: light-dark(#ffffff, #0d1117);"><code class="language-elixir" translate="no" tabindex="0"><span class="l-line" data-line="1"><span style="color: light-dark(#1f2328, #e6edf3);">[</span></span>
<span class="l-line" data-line="2">  <span style="color: light-dark(#0550ae, #79c0ff);">layers: </span><span style="color: light-dark(#1f2328, #e6edf3);">[</span></span>
<span class="l-line" data-line="3">    <span style="color: light-dark(#0550ae, #79c0ff);">site: </span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;Blog.Site&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span></span>
<span class="l-line" data-line="4">    <span style="color: light-dark(#0550ae, #79c0ff);">plugins: </span><span style="color: light-dark(#1f2328, #e6edf3);">[</span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;Blog.SocialImages&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0a3069, #a5d6ff);">&quot;Blog.Markdown.*&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">]</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span></span>
<span class="l-line" data-line="5">    <span style="color: light-dark(#0550ae, #79c0ff);">previews: </span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;Blog.LinkPreview*&quot;</span></span>
<span class="l-line" data-line="6">  <span style="color: light-dark(#1f2328, #e6edf3);">]</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span></span>
<span class="l-line" data-line="7">  <span style="color: light-dark(#0550ae, #79c0ff);">deps: </span><span style="color: light-dark(#1f2328, #e6edf3);">[</span><span style="color: light-dark(#0550ae, #79c0ff);">forbidden: </span><span style="color: light-dark(#1f2328, #e6edf3);">[{</span><span style="color: light-dark(#0550ae, #79c0ff);">:site</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">:plugins</span><span style="color: light-dark(#1f2328, #e6edf3);">}</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#1f2328, #e6edf3);">{</span><span style="color: light-dark(#0550ae, #79c0ff);">:site</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">:previews</span><span style="color: light-dark(#1f2328, #e6edf3);">}</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#1f2328, #e6edf3);">{</span><span style="color: light-dark(#0550ae, #79c0ff);">:plugins</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">:previews</span><span style="color: light-dark(#1f2328, #e6edf3);">}]]</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span></span>
<span class="l-line" data-line="8">  <span style="color: light-dark(#0550ae, #79c0ff);">calls: </span><span style="color: light-dark(#1f2328, #e6edf3);">[</span><span style="color: light-dark(#0550ae, #79c0ff);">forbidden: </span><span style="color: light-dark(#1f2328, #e6edf3);">[{</span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;Blog.LinkPreview&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0a3069, #a5d6ff);">&quot;Req.get/2&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">}]]</span></span>
<span class="l-line" data-line="9"><span style="color: light-dark(#1f2328, #e6edf3);">]</span></span></code></pre>
<p>The last line is a trap I set for the example, and the check walks into it:</p>
<pre class="lumis lumis-themes dark light" style="color: light-dark(#1f2328, #e6edf3); background-color: light-dark(#ffffff, #0d1117);"><code class="language-bash" translate="no" tabindex="0"><span class="l-line" data-line="1"><span style="color: light-dark(#1f2328, #e6edf3);">$</span> <span style="color: light-dark(#1f2328, #e6edf3);">mix</span> <span style="color: light-dark(#1f2328, #e6edf3);">reach.check</span> <span style="color: light-dark(#1f2328, #e6edf3);">--arch</span></span>
<span class="l-line" data-line="2">  <span style="color: light-dark(#0550ae, #79c0ff);">1</span> violation<span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0550ae, #79c0ff);">s</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span></span>
<span class="l-line" data-line="3">  <span style="color: light-dark(#0550ae, #79c0ff);">lib/blog/link_preview.ex:88</span> <span style="color: light-dark(#1f2328, #e6edf3);">Blog.LinkPreview</span> <span style="color: light-dark(#1f2328, #e6edf3);">calls</span> <span style="color: light-dark(#1f2328, #e6edf3);">Req.get/2</span> <span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0550ae, #79c0ff);">configured</span> <span style="color: light-dark(#1f2328, #e6edf3);">forbidden</span> <span style="color: light-dark(#1f2328, #e6edf3);">call</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span></span>
<span class="l-line" data-line="4"><span style="color: light-dark(#0550ae, #79c0ff);">**</span> <span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0550ae, #79c0ff);">Mix</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span> <span style="color: light-dark(#0550ae, #79c0ff);">Architecture</span> <span style="color: light-dark(#1f2328, #e6edf3);">policy</span> <span style="color: light-dark(#1f2328, #e6edf3);">failed</span></span></code></pre>
<p>Smell findings carry the label in their JSON, the form an agent reads. Reach found these two in this site while I was writing the paragraph:</p>
<pre class="lumis lumis-themes dark light" style="color: light-dark(#1f2328, #e6edf3); background-color: light-dark(#ffffff, #0d1117);"><code class="language-json" translate="no" tabindex="0"><span class="l-line" data-line="1">{&quot;kind&quot;: &quot;suboptimal&quot;, &quot;location&quot;: &quot;lib/blog/highlight.ex:33&quot;,</span>
<span class="l-line" data-line="2"> &quot;message&quot;: &quot;Enum.map_join/3 defaults to empty separator; remove the \&quot;\&quot; argument&quot;,</span>
<span class="l-line" data-line="3"> &quot;remediation_safety&quot;: &quot;equivalent&quot;}</span>
<span class="l-line" data-line="4">{&quot;kind&quot;: &quot;suboptimal&quot;, &quot;location&quot;: &quot;lib/blog/link_preview.ex:179&quot;,</span>
<span class="l-line" data-line="5"> &quot;message&quot;: &quot;String.split/2 |&gt; hd/1 splits the entire string; use String.split/3 with parts: 2&quot;,</span>
<span class="l-line" data-line="6"> &quot;remediation_safety&quot;: &quot;review_only&quot;}</span></code></pre>
<p>The first is a proven rewrite. The second needs a look, because <code>parts: 2</code> changes what the function returns when the string has more than one separator.</p>
<p>But adding more checks creates its own risk. A false positive is annoying for a human, but an agent may obey it and make the code worse just to silence the warning. A rule that looks convincing in a few hand-written examples may fail on perfectly reasonable code in a real project.</p>
<p>That is why I built <a href="https://github.com/elixir-vibe/exograph">Exograph</a>: local <a href="https://codeql.github.com">CodeQL</a>-style code search for Elixir, backed by DuckDB through <a href="https://github.com/elixir-vibe/quackdb">QuackDB</a> and <a href="https://github.com/elixir-vibe/ex_ast">ExAST</a>. It indexes the entire public <a href="https://hex.pm">Hex</a> package ecosystem and queries it by structure, similarity, and call graph. Running a proposed rule across that corpus is how I find false positives and decide whether it is reliable enough to keep.</p>
<p>As I was finishing this post, José Valim argued in <a href="https://dashbit.co/blog/evolving-ai-era">“Evolving programming languages in the AI era”</a> that agents need stronger guarantees and a program database with a query language more than an editor protocol built for humans. <a href="https://github.com/elixir-vibe/reach">Reach</a> and <a href="https://github.com/elixir-vibe/exograph">Exograph</a> are my attempt at that database for Elixir: facts about calls, data flow, effects, and architecture that an agent can query.</p>
<p>The same tools are used to check themselves. My projects combine the compiler, tests, Dialyzer, <a href="https://github.com/elixir-vibe/ex_dna">ExDNA</a>, <a href="https://github.com/elixir-vibe/ex_slop">ExSlop</a>, <a href="https://github.com/elixir-vibe/reach">Reach</a>, and architecture rules. <a href="https://github.com/elixir-vibe/vibe_kit">VibeKit</a> installs that setup into a new or existing project with one command.</p>
<p>None of this makes blind vibe coding safe. It reduces some kinds of ambiguity, catches structural drift earlier, and gives agents more precise feedback when their work does not fit the rest of the system.</p>
<h2 id="frontend-tooling-in-elixir">Frontend tooling in Elixir<a href="#frontend-tooling-in-elixir" aria-label="Link to heading 'Frontend tooling in Elixir'" data-heading-content="Frontend tooling in Elixir" class="anchor"></a></h2>
<p>Choosing Elixir did not remove the need for JavaScript. The npm ecosystem contains too much useful work, and Vue is a good abstraction for building interfaces. Rewriting all of it in Elixir would make no sense.</p>
<p>The problem was the separate operational world around it: Node processes, package managers, framework compilers, bundlers, and CSS tools, each with its own configuration and lifecycle. Calling JavaScript from Elixir is easy; start a Node process and exchange JSON. I wanted JavaScript execution to be observable and controllable as part of the same system.</p>
<p>That led to <a href="https://github.com/elixir-volt/quickbeam">QuickBEAM</a>.</p>
<p>In <a href="https://github.com/elixir-volt/quickbeam">QuickBEAM</a>, JavaScript runtimes and contexts behave like part of an OTP application. They have process ownership, participate in supervision trees, exchange messages with BEAM processes, and can be monitored, stopped, restarted, and inspected. JavaScript values map directly to BEAM terms.</p>
<p>Execution can also be constrained by memory and an instruction budget per call, in the spirit of BEAM reductions, so runaway JavaScript does not have unlimited control of the host application. Large numbers of lightweight contexts can share a small pool of runtime threads. Browser APIs such as workers, timers, storage, and networking are backed by OTP primitives, and runtimes can call each other across Erlang nodes.</p>
<p>In August it went a step further, with an interpreter that runs QuickJS bytecode as BEAM code. Each call gets a fresh heap, the scheduler can preempt it like any other process, and a failure is contained to one evaluation. I wrote about it in <a href="/writing/javascript-as-beam-code/">“JavaScript as BEAM code”</a>.</p>
<p>JavaScript remains available where the ecosystem requires it, but it no longer disappears into an opaque Node sidecar.</p>
<p>Package management came next. <a href="https://github.com/elixir-volt/npm_ex"><code>npm_ex</code></a> resolves, fetches, caches, and links npm packages from Mix, with no Node on the machine. It started as a small library for inspecting <code>package.json</code> and dependency trees, then grew into most of a package manager: a PubGrub resolver, a reproducible lockfile, and a global cache. It is also stricter than npm by default. Lifecycle scripts never run on their own, transitive git and URL dependencies are blocked, and an audit checks packages against the OSV list of malicious releases.</p>
<p>I also created Elixir bindings for the Rust tools that already do much of the real work in modern frontend toolchains: <a href="https://oxc.rs">OXC</a> for JavaScript and TypeScript, <a href="https://github.com/ubugeeei/vize">Vize</a> for Vue, and <a href="https://tailwindcss.com">Tailwind</a>’s Oxide scanner, which finds class names in source files. These projects did not need to be rewritten; they needed APIs the BEAM could call directly.</p>
<p><a href="https://github.com/elixir-volt/volt">Volt</a> assembles these pieces into one frontend toolchain that replaces esbuild, the Tailwind CLI, and Node.js: a development server with hot module replacement, linting, and production builds for TypeScript, Vue, React, Svelte, and Solid. JavaScript tests run inside <code>mix test</code>, and since September every error in the chain, from OXC through <a href="https://github.com/elixir-volt/quickbeam">QuickBEAM</a> and Volt to <a href="https://github.com/elixir-volt/phoenix_vapor">PhoenixVapor</a>, has the same diagnostic shape with file, line, and column. The toolchain starts with the application and can be configured, observed, and extended from Elixir.</p>
<p>&lt;.building_this_year_volt_tree /&gt;</p>
<p>This removes one boundary, but frontend and backend code can still describe two halves of the same behavior and quietly disagree.</p>
<p><a href="https://github.com/elixir-volt/phoenix_vapor">PhoenixVapor</a> explores a more direct bridge. It compiles Vue template syntax into native <a href="https://hexdocs.pm/phoenix_live_view">Phoenix LiveView</a> rendering structures, so it uses the same diff protocol with no wrapper elements. It has four modes: Vue syntax with no client JavaScript, server-side reactivity through <a href="https://github.com/elixir-volt/quickbeam">QuickBEAM</a>, a full Vue runtime on the server that renders third-party component libraries without shipping them to the browser, and a hybrid where the server owns application data while the browser owns local interface state.</p>
<p>&lt;.building_this_year_vapor_wire /&gt;</p>
<p>This connects back to <a href="https://github.com/dannote/vue-pencil">VuePencil</a>. A component created there can remain a real Vue component. It can become an ordinary client-side Vue application built by <a href="https://github.com/elixir-volt/volt">Volt</a>, a Vue island embedded in a server-rendered <a href="https://www.phoenixframework.org">Phoenix</a> page, or a template compiled into LiveView.</p>
<h2 id="the-coding-environment">The coding environment<a href="#the-coding-environment" aria-label="Link to heading 'The coding environment'" data-heading-content="The coding environment" class="anchor"></a></h2>
<p>I am a big fan of <a href="https://github.com/badlogic/pi-mono">Pi</a>, Mario Zechner’s coding agent. Its core is deliberately small, and extensions, skills, and prompt files let me adapt it to my workflow, so I first built <a href="https://github.com/elixir-vibe/pi-elixir">pi-elixir</a> as a <a href="https://github.com/badlogic/pi-mono">Pi</a> extension.</p>
<p><a href="https://github.com/elixir-vibe/pi-elixir">pi-elixir</a> connects <a href="https://github.com/badlogic/pi-mono">Pi</a> to the BEAM through three tools: evaluate Elixir, and search and rewrite the AST with <a href="https://github.com/elixir-vibe/ex_ast">ExAST</a>. Evaluation can target the project, the running application, or a remote node, and values persist between calls like an IEx or Livebook session. The target project needs no change to its mix file.</p>
<p>But the more I extended the harness, the less natural its underlying execution model felt for the direction I wanted to take.</p>
<p>A coding harness is a concurrent, long-running system: model streams, terminal input, tool execution, background work, cancellation, retries, persistence, and eventually other agents. <a href="https://github.com/badlogic/pi-mono">Pi</a> implements all of this carefully in JavaScript, but the work still travels through layers of promises, callbacks, and terminal redraws inside one process, with ownership and failure boundaries maintained by convention. OTP has a more direct model. A session is a process, a model request and each tool execution are supervised children, and terminal, browser, and remote clients observe the session without owning it. Processes can be monitored, cancelled, restarted, or allowed to fail independently, and the scheduler preempts them.</p>
<p>I built <a href="https://github.com/elixir-vibe/vibe">Vibe</a> to explore this architecture without the constraints of an existing harness.</p>
<p>In <a href="https://github.com/elixir-vibe/vibe">Vibe</a>, sessions, agents, subagents, commands, and interfaces are OTP processes. Agents can start other agents and communicate through messages. They can run on one node or communicate across machines through Erlang distribution and SSH. Closing a terminal does not have to stop the work, and a failing subagent does not have to destroy its parent session. A background server owns the sessions, like tmux, so several terminals or a LiveView console can attach to the same one, and memory and past transcripts are searchable. Vibe can also run its own checks, patch its own code, and hot-reload the result, which makes it a first step toward agents that modify and improve themselves.</p>
<p>&lt;.building_this_year_vibe_tree /&gt;</p>
<p>OpenAI reached the same conclusion. The reference implementation of <a href="https://github.com/openai/symphony">Symphony</a>, its system for supervising long-running coding-agent work, is written in Elixir.</p>
<p><a href="https://github.com/elixir-vibe/vibe">Vibe</a> is useful, but it is also an experiment. I do not want to force users to replace a mature harness with my half-finished one just to test each hypothesis. When an idea works in <a href="https://github.com/elixir-vibe/vibe">Vibe</a>, I can bring it back into <a href="https://github.com/elixir-vibe/pi-elixir">pi-elixir</a> and test it inside <a href="https://github.com/badlogic/pi-mono">Pi</a>. The newer <a href="https://github.com/elixir-vibe/pi-elixir">pi-elixir</a> combines <a href="https://github.com/badlogic/pi-mono">Pi</a>’s model support, interface, and extension system with more of the BEAM-native runtime and structural tooling explored in <a href="https://github.com/elixir-vibe/vibe">Vibe</a>.</p>
<p><a href="https://github.com/elixir-vibe/tilde">Tilde</a> develops another part of this architecture: separating an agent session from any particular interface.</p>
<p>A <a href="https://github.com/elixir-vibe/tilde">Tilde</a> session is a stream of semantic events: a message was added, a tool started, output arrived, a choice was requested, or an agent completed its work. The same session can be rendered in a terminal, in a browser with LiveView, over SSH, or as structured data. Sessions persist through <a href="https://github.com/elixir-vibe/quackdb">QuackDB</a>, so one can be resumed after a restart, searched, and served over LiveView and SSH at the same time.</p>
<h2 id="common-storage-and-apis">Common storage and APIs<a href="#common-storage-and-apis" aria-label="Link to heading 'Common storage and APIs'" data-heading-content="Common storage and APIs" class="anchor"></a></h2>
<p>A platform for building several products should not require a separate collection of managed services for each one. I wanted the default setup to remain cheap, portable, and easy to run locally.</p>
<p>For storage, I settled on <a href="https://duckdb.org">DuckDB</a>.</p>
<p>DuckDB combines much of the SQL surface I expect from PostgreSQL with the portability of SQLite. A complete database can live in one file, but it still supports analytical queries, full-text search, geospatial operations, Parquet, and direct access to S3. For many small and medium products, it can handle both ordinary application data and serious analytics without requiring a separate analytical cluster.</p>
<pre class="lumis lumis-themes dark light" style="color: light-dark(#1f2328, #e6edf3); background-color: light-dark(#ffffff, #0d1117);"><code class="language-sql" translate="no" tabindex="0"><span class="l-line" data-line="1"><span style="color: light-dark(#57606a, #8b949e);">-- A campaign funnel, straight from a month of Parquet files on S3.</span></span>
<span class="l-line" data-line="2"><span style="color: light-dark(#cf222e, #ff7b72);">SELECT</span> <span style="color: light-dark(#0550ae, #79c0ff);">utm_campaign</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span></span>
<span class="l-line" data-line="3">       <span style="color: light-dark(#1f2328, #e6edf3);">count</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#cf222e, #ff7b72);">DISTINCT</span> <span style="color: light-dark(#0550ae, #79c0ff);">session_id</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span>                                <span style="color: light-dark(#cf222e, #ff7b72);">AS</span> <span style="color: light-dark(#0550ae, #79c0ff);">sessions</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span></span>
<span class="l-line" data-line="4">       <span style="color: light-dark(#1f2328, #e6edf3);">count</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#cf222e, #ff7b72);">DISTINCT</span> <span style="color: light-dark(#0550ae, #79c0ff);">session_id</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span> <span style="color: light-dark(#cf222e, #ff7b72);">FILTER</span> <span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#cf222e, #ff7b72);">WHERE</span> <span style="color: light-dark(#0550ae, #79c0ff);">name</span> <span style="color: light-dark(#0550ae, #79c0ff);">=</span> <span style="color: light-dark(#0a3069, #a5d6ff);">&#39;signup&#39;</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span> <span style="color: light-dark(#cf222e, #ff7b72);">AS</span> <span style="color: light-dark(#0550ae, #79c0ff);">signups</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span></span>
<span class="l-line" data-line="5">       <span style="color: light-dark(#1f2328, #e6edf3);">count</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#cf222e, #ff7b72);">DISTINCT</span> <span style="color: light-dark(#0550ae, #79c0ff);">session_id</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span> <span style="color: light-dark(#cf222e, #ff7b72);">FILTER</span> <span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#cf222e, #ff7b72);">WHERE</span> <span style="color: light-dark(#0550ae, #79c0ff);">name</span> <span style="color: light-dark(#0550ae, #79c0ff);">=</span> <span style="color: light-dark(#0a3069, #a5d6ff);">&#39;paid&#39;</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span>   <span style="color: light-dark(#cf222e, #ff7b72);">AS</span> <span style="color: light-dark(#0550ae, #79c0ff);">paid</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span></span>
<span class="l-line" data-line="6">       <span style="color: light-dark(#1f2328, #e6edf3);">round</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0550ae, #79c0ff);">100.0</span> <span style="color: light-dark(#0550ae, #79c0ff);">*</span> <span style="color: light-dark(#0550ae, #79c0ff);">paid</span> <span style="color: light-dark(#0550ae, #79c0ff);">/</span> <span style="color: light-dark(#0550ae, #79c0ff);">sessions</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">1</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span>                         <span style="color: light-dark(#cf222e, #ff7b72);">AS</span> <span style="color: light-dark(#0550ae, #79c0ff);">conversion</span></span>
<span class="l-line" data-line="7"><span style="color: light-dark(#cf222e, #ff7b72);">FROM</span> <span style="color: light-dark(#1f2328, #e6edf3);">read_parquet</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0a3069, #a5d6ff);">&#39;s3://analytics/events/2026-09-*.parquet&#39;</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span></span>
<span class="l-line" data-line="8"><span style="color: light-dark(#cf222e, #ff7b72);">GROUP</span> <span style="color: light-dark(#cf222e, #ff7b72);">BY</span> <span style="color: light-dark(#cf222e, #ff7b72);">ALL</span></span>
<span class="l-line" data-line="9"><span style="color: light-dark(#1f2328, #e6edf3);">QUALIFY</span> row_number<span style="color: light-dark(#1f2328, #e6edf3);">()</span> <span style="color: light-dark(#cf222e, #ff7b72);">OVER</span> <span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#cf222e, #ff7b72);">ORDER</span> <span style="color: light-dark(#cf222e, #ff7b72);">BY</span> <span style="color: light-dark(#0550ae, #79c0ff);">conversion</span> <span style="color: light-dark(#0550ae, #79c0ff);">DESC</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span> <span style="color: light-dark(#0550ae, #79c0ff);">&lt;=</span> <span style="color: light-dark(#0550ae, #79c0ff);">10</span><span style="color: light-dark(#1f2328, #e6edf3);">;</span></span></code></pre>
<p><a href="https://github.com/elixir-vibe/quackdb">QuackDB</a> makes DuckDB usable as part of an Elixir application: an OTP-supervised DuckDB process, a <code>DBConnection</code> client, an Ecto adapter, native append and streaming, and a query DSL so most work never touches raw SQL.</p>
<pre class="lumis lumis-themes dark light" style="color: light-dark(#1f2328, #e6edf3); background-color: light-dark(#ffffff, #0d1117);"><code class="language-elixir" translate="no" tabindex="0"><span class="l-line" data-line="1"><span style="color: light-dark(#6639ba, #d2a8ff);">from</span> <span style="color: light-dark(#1f2328, #e6edf3);">e</span> <span style="color: light-dark(#cf222e, #ff7b72);">in</span> <span style="color: light-dark(#953800, #ffa657);">Source</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#6639ba, #d2a8ff);">parquet</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;s3://analytics/events/2026-09-*.parquet&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span></span>
<span class="l-line" data-line="2">  <span style="color: light-dark(#0550ae, #79c0ff);">group_by: </span><span style="color: light-dark(#1f2328, #e6edf3);">e</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#1f2328, #e6edf3);">utm_campaign</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span></span>
<span class="l-line" data-line="3">  <span style="color: light-dark(#0550ae, #79c0ff);">select: </span><span style="color: light-dark(#1f2328, #e6edf3);">%</span><span style="color: light-dark(#1f2328, #e6edf3);">{</span></span>
<span class="l-line" data-line="4">    <span style="color: light-dark(#0550ae, #79c0ff);">campaign: </span><span style="color: light-dark(#1f2328, #e6edf3);">e</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#1f2328, #e6edf3);">utm_campaign</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span></span>
<span class="l-line" data-line="5">    <span style="color: light-dark(#0550ae, #79c0ff);">sessions: </span><span style="color: light-dark(#6639ba, #d2a8ff);">count</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#1f2328, #e6edf3);">e</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#1f2328, #e6edf3);">session_id</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">:distinct</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span></span>
<span class="l-line" data-line="6">    <span style="color: light-dark(#0550ae, #79c0ff);">signups: </span><span style="color: light-dark(#6639ba, #d2a8ff);">filter</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#6639ba, #d2a8ff);">count</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#1f2328, #e6edf3);">e</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#1f2328, #e6edf3);">session_id</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">:distinct</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#1f2328, #e6edf3);">e</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#1f2328, #e6edf3);">name</span> <span style="color: light-dark(#0550ae, #79c0ff);">==</span> <span style="color: light-dark(#0a3069, #a5d6ff);">&quot;signup&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span></span>
<span class="l-line" data-line="7">    <span style="color: light-dark(#0550ae, #79c0ff);">paid: </span><span style="color: light-dark(#6639ba, #d2a8ff);">filter</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#6639ba, #d2a8ff);">count</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#1f2328, #e6edf3);">e</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#1f2328, #e6edf3);">session_id</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">:distinct</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#1f2328, #e6edf3);">e</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#1f2328, #e6edf3);">name</span> <span style="color: light-dark(#0550ae, #79c0ff);">==</span> <span style="color: light-dark(#0a3069, #a5d6ff);">&quot;paid&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span></span>
<span class="l-line" data-line="8">  <span style="color: light-dark(#1f2328, #e6edf3);">}</span></span></code></pre>
<p>DuckDB is becoming the common storage layer across the platform. <a href="https://github.com/elixir-vibe/exograph">Exograph</a>’s index lives in it, <a href="https://github.com/elixir-vibe/pi-elixir">pi-elixir</a> mirrors its sessions into it, <a href="https://github.com/elixir-vibe/tilde">Tilde</a> persists through it, <a href="https://github.com/elixir-vibe/llm_proxy">LLMProxy</a>’s standalone mode keeps usage in it, and the analytics fork is moving onto it. <a href="https://github.com/elixir-vibe/vibe">Vibe</a> still keeps its sessions in SQLite.</p>
<p>I started <a href="https://github.com/elixir-vibe/quackdb">QuackDB</a> while DuckDB’s Quack client-server protocol was still experimental. It has since reached 1.0 in <a href="https://duckdb.org/2026/09/02/try-duckdb-20-alpha.html">DuckDB 2.0</a>, and with DuckLabs <a href="https://aws.amazon.com/blogs/big-data/aws-and-ducklabs-building-the-future-of-analytics-together/">joining AWS</a> while the project stays MIT-licensed under the independent DuckDB Foundation, I consider it a safe long-term bet.</p>
<p>Storage was one shared service. Model access is the other: not every product needs an LLM, but most now call one, and every product eventually depends on external APIs.</p>
<p><a href="https://github.com/elixir-vibe/llm_proxy">LLMProxy</a> is one execution path for every model call, in the spirit of <a href="https://www.litellm.ai">LiteLLM</a> but Elixir-native: it runs inside the application, or standalone with OpenAI- and Anthropic-compatible endpoints. Callers ask for <code>fast</code>, <code>smart</code>, or <code>cheap</code>. Routing, fallbacks, quotas, and accounting happen behind those names, so a provider can change without touching a caller, and the founder sees where every token went.</p>
<p>I found the same pattern repeated with non-LLM services. A startup gradually accumulates APIs for email, advertising, payments, image generation, vectorization, search, and hosting. Each integration comes with its own authentication, retries, limits, credentials, errors, and accounting.</p>
<p>Egress is the next step, and it is still a design: an external service described once as a set of typed operations, such as creating a campaign or sending an email, that produce both an Elixir client and a proxy route through the same runtime path for authentication, retries, quotas, tracing, and accounting. An agent would call an operation with a contract and never see the URL or the provider’s authentication scheme. The repository stays private until the first operations run.</p>
<h2 id="deployment-without-another-platform">Deployment without another platform<a href="#deployment-without-another-platform" aria-label="Link to heading 'Deployment without another platform'" data-heading-content="Deployment without another platform" class="anchor"></a></h2>
<p>Once the application is built, it still has to run somewhere.</p>
<p>For small products, deployment often introduces another large stack: container registries, orchestration, managed databases, queues, proxies, secret stores, and several dashboards. These tools can be justified at a certain scale, but I do not want every experiment to begin with them.</p>
<p>Elixir already has a good deployment unit: an OTP release containing the application, its dependencies, and the runtime it needs.</p>
<p><a href="https://github.com/elixir-vibe/release_kit">ReleaseKit</a> turns a Mix release into a repeatable, deployment-neutral artifact. It produces an ordinary tarball and a small manifest describing how to run it, which environment it expects, and how to check its health. It deliberately knows nothing about servers, users, systemd, or reverse proxies.</p>
<p><a href="https://github.com/elixir-vibe/host_kit">HostKit</a> handles the other half. It describes a Linux host in Elixir: packages, users, services, secrets, firewall rules, and <a href="https://caddyserver.com">Caddy</a> reverse-proxy routes. It reads the current state, produces a plan, applies the reviewed plan locally or over SSH, and can bootstrap a bare machine with no Elixir on it. A host is ordinary Elixir:</p>
<pre class="lumis lumis-themes dark light" style="color: light-dark(#1f2328, #e6edf3); background-color: light-dark(#ffffff, #0d1117);"><code class="language-elixir" translate="no" tabindex="0"><span class="l-line" data-line="1"><span style="color: light-dark(#cf222e, #ff7b72);">use</span> <span style="color: light-dark(#953800, #ffa657);">HostKit.DSL</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">providers: </span><span style="color: light-dark(#1f2328, #e6edf3);">[</span><span style="color: light-dark(#953800, #ffa657);">HostKit.Providers.Caddy</span><span style="color: light-dark(#1f2328, #e6edf3);">]</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="2"></span>
<span class="l-line" data-line="3"><span style="color: light-dark(#6639ba, #d2a8ff);">project</span> <span style="color: light-dark(#0550ae, #79c0ff);">:prod</span> <span style="color: light-dark(#cf222e, #ff7b72);">do</span></span>
<span class="l-line" data-line="4">  <span style="color: light-dark(#6639ba, #d2a8ff);">host</span> <span style="color: light-dark(#0550ae, #79c0ff);">:app</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">at: </span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;app.example.com&quot;</span> <span style="color: light-dark(#cf222e, #ff7b72);">do</span></span>
<span class="l-line" data-line="5">    <span style="color: light-dark(#6639ba, #d2a8ff);">ssh</span> <span style="color: light-dark(#cf222e, #ff7b72);">do</span></span>
<span class="l-line" data-line="6">      <span style="color: light-dark(#6639ba, #d2a8ff);">user</span> <span style="color: light-dark(#0a3069, #a5d6ff);">&quot;root&quot;</span></span>
<span class="l-line" data-line="7">      <span style="color: light-dark(#6639ba, #d2a8ff);">identity_file</span> <span style="color: light-dark(#953800, #ffa657);">Path</span><span style="color: light-dark(#0550ae, #79c0ff);">.</span><span style="color: light-dark(#6639ba, #d2a8ff);">expand</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;~/.ssh/id_ed25519&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">)</span></span>
<span class="l-line" data-line="8">    <span style="color: light-dark(#cf222e, #ff7b72);">end</span></span>
<span class="l-line" data-line="9">  <span style="color: light-dark(#cf222e, #ff7b72);">end</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="10"></span>
<span class="l-line" data-line="11">  <span style="color: light-dark(#6639ba, #d2a8ff);">service</span> <span style="color: light-dark(#0550ae, #79c0ff);">:api</span> <span style="color: light-dark(#cf222e, #ff7b72);">do</span></span>
<span class="l-line" data-line="12">    <span style="color: light-dark(#6639ba, #d2a8ff);">daemon</span> <span style="color: light-dark(#0550ae, #79c0ff);">:api</span> <span style="color: light-dark(#cf222e, #ff7b72);">do</span></span>
<span class="l-line" data-line="13">      <span style="color: light-dark(#6639ba, #d2a8ff);">exec</span> <span style="color: light-dark(#6639ba, #d2a8ff);">argv</span><span style="color: light-dark(#1f2328, #e6edf3);">(</span><span style="color: light-dark(#0a3069, #a5d6ff);">&quot;/opt/api/bin/server&quot;</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">opts: </span><span style="color: light-dark(#1f2328, #e6edf3);">[</span><span style="color: light-dark(#0550ae, #79c0ff);">port: </span><span style="color: light-dark(#0550ae, #79c0ff);">4000</span><span style="color: light-dark(#1f2328, #e6edf3);">])</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="14"></span>
<span class="l-line" data-line="15">      <span style="color: light-dark(#6639ba, #d2a8ff);">isolate</span> <span style="color: light-dark(#cf222e, #ff7b72);">do</span></span>
<span class="l-line" data-line="16">        <span style="color: light-dark(#6639ba, #d2a8ff);">memory_max</span> <span style="color: light-dark(#0a3069, #a5d6ff);">&quot;512M&quot;</span></span>
<span class="l-line" data-line="17">        <span style="color: light-dark(#6639ba, #d2a8ff);">network</span> <span style="color: light-dark(#0550ae, #79c0ff);">:loopback</span></span>
<span class="l-line" data-line="18">      <span style="color: light-dark(#cf222e, #ff7b72);">end</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="19"></span>
<span class="l-line" data-line="20">      <span style="color: light-dark(#6639ba, #d2a8ff);">listen</span> <span style="color: light-dark(#0550ae, #79c0ff);">:http</span><span style="color: light-dark(#1f2328, #e6edf3);">,</span> <span style="color: light-dark(#0550ae, #79c0ff);">port: </span><span style="color: light-dark(#0550ae, #79c0ff);">4000</span></span>
<span class="l-line" data-line="21">    <span style="color: light-dark(#cf222e, #ff7b72);">end</span></span>
<span class="l-line" style="display: inline-block; width: 100%; min-height: 1lh; vertical-align: top;" data-line="22"></span>
<span class="l-line" data-line="23">    <span style="color: light-dark(#6639ba, #d2a8ff);">caddy_site</span> <span style="color: light-dark(#0a3069, #a5d6ff);">&quot;api.example.com&quot;</span> <span style="color: light-dark(#cf222e, #ff7b72);">do</span></span>
<span class="l-line" data-line="24">      <span style="color: light-dark(#6639ba, #d2a8ff);">reverse_proxy</span> <span style="color: light-dark(#0550ae, #79c0ff);">:http</span></span>
<span class="l-line" data-line="25">    <span style="color: light-dark(#cf222e, #ff7b72);">end</span></span>
<span class="l-line" data-line="26">  <span style="color: light-dark(#cf222e, #ff7b72);">end</span></span>
<span class="l-line" data-line="27"><span style="color: light-dark(#cf222e, #ff7b72);">end</span></span></code></pre>
<p>The result is ordinary Linux: services under systemd with restart policies, resource limits, filesystem restrictions, and network isolation. Everything is a library call first, and the Mix tasks are wrappers. So the questions an agent asks the runtime have host-level answers too: which ports listen, which services failed, what a plan would change, and how to roll it back.</p>
<p><a href="https://github.com/elixir-vibe/host_kit">HostKit</a> is still a beta, but it runs my own infrastructure. It makes deploying a small or medium product cheap and understandable, with infrastructure as observable as every other part of the platform.</p>
<p>Once the product is running, the more interesting question begins: what are users doing, where did they come from, what is breaking, and what should change next?</p>
<h2 id="from-analytics-to-the-next-decision">From analytics to the next decision<a href="#from-analytics-to-the-next-decision" aria-label="Link to heading 'From analytics to the next decision'" data-heading-content="From analytics to the next decision" class="anchor"></a></h2>
<p>The answers are usually divided among unrelated systems. Advertising platforms know the campaign and its cost. Web analytics knows the landing page and conversion. Session-recording software knows what happened in the browser. Application monitoring has the errors and traces. The BEAM knows the live process state. An LLM provider knows how many tokens were spent. None of them has the complete context.</p>
<p>I started experimenting with this in <a href="https://github.com/dannote/analytics/tree/duckdb-analytics">a branch of my fork</a> of <a href="https://plausible.io">Plausible Analytics</a>. ClickHouse is gone from that branch: analytics storage, imports, exports, and the job queue run on DuckDB through <a href="https://github.com/elixir-vibe/quackdb">QuackDB</a>, while Plausible’s own PostgreSQL stays for now. The goal is to keep traffic, funnels, attribution, conversions, and other product analytics in the same portable data layer as the rest of the platform, without a ClickHouse cluster for every product.</p>
<p>Aggregate analytics still cannot explain what happened to one particular user. For that, I built <a href="https://github.com/elixir-vibe/phoenix_replay">PhoenixReplay</a>.</p>
<p>In a Phoenix LiveView application, much of the interface state lives on the backend, and the browser displays updates produced from that state. So instead of recording clicks and DOM changes in the browser, <a href="https://github.com/elixir-vibe/phoenix_replay">PhoenixReplay</a> records the server’s assigns, sanitizes them, and re-renders them later. The replay shows each screen as the server produced it, next to the state behind it. Because nothing runs in the browser, client-only JavaScript state is outside the recording.</p>
<div class="x-post">
<blockquote>
<p>New package! PhoenixReplay — session recording and replay for Phoenix LiveView.</p>
<p>→ Records assigns server-side, replays by re-rendering the actual view — pixel-perfect, not a DOM approximation<br />
→ Zero client-side JS — no bundle size impact, invisible to users<br />
→ See actual server state during replay: changesets, Ecto structs, form data<br />
→ Navigation, page transitions, live_patch — all captured in one session automatically<br />
→ 30s session = 8 KB</p>
<p>rrweb and friends record DOM mutations client-side: they fight CORS, break on shadow DOM, drift when assets change, and produce approximate replays with visual glitches. None of that applies here — LiveView templates are pure functions, so same assigns = same HTML. The BEAM just keeps the state.</p>
<p><img src="images/x/dan_note-2031256903192342649.jpg" alt="A frame from the PhoenixReplay demo video: a recorded LiveView session playing back next to the server state behind it." /></p>
<p>— Danila Poyarkov (@dan_note), <a href="https://x.com/dan_note/status/2031256903192342649">10 March 2026</a></p>
</blockquote>
</div>
<p>The next step is to connect these sources rather than open them in separate dashboards.</p>
<p>Suppose a customer arrives through a paid campaign, begins registration, gets stuck, and leaves. I want to see where they came from and how much that acquisition cost, replay what they saw, screen by screen, from the backend state that produced it, inspect related errors and traces, and follow the relevant code path. If an LLM or another paid API participated in the request, its latency and cost should be visible too.</p>
<p>&lt;.building_this_year_customer_path /&gt;</p>
<p><a href="https://github.com/elixir-vibe/incant">Incant</a> is the common admin interface for this. Resources, dashboards, datasets, and actions are modules, and a service declares what its admin pages contain, and a standalone Incant host renders them. <a href="https://github.com/elixir-vibe/llm_proxy">LLMProxy</a>’s provider usage already lives there; replay sessions, telemetry, campaign performance, agent state, and infrastructure are meant to follow through the same Elixir APIs.</p>
<p>An agent can use the same data. A background agent could notice that conversion from a campaign dropped, identify the affected landing page, inspect recent sessions, find where users started abandoning the funnel, and propose a change with the evidence that led to it. Each one arrives as a suggestion with its evidence attached, and the founder approves it or not.</p>
<h2 id="how-this-differs-from-figma-lovable-and-replit">How this differs from Figma, Lovable, and Replit<a href="#how-this-differs-from-figma-lovable-and-replit" aria-label="Link to heading 'How this differs from Figma, Lovable, and Replit'" data-heading-content="How this differs from Figma, Lovable, and Replit" class="anchor"></a></h2>
<p>Figma is a different case. Once <code>.fig</code> is a commodity, a design no longer belongs to the editor it was drawn in.</p>
<p>Going through Figma’s own MCP server is no safer today. Figma’s remote MCP server decides which agents may connect at all: sign-in works only for client names on its list, such as Claude Code or Codex. This week, when Mario Zechner tried to connect <a href="https://github.com/badlogic/pi-mono">Pi</a>, his open-source coding agent, Figma refused it because <a href="https://github.com/badlogic/pi-mono">Pi</a> identifies itself as <code>pi</code>.</p>
<div class="x-post">
<blockquote>
<p>today in MCP land ...</p>
<p>thing are better compared to a year ago, but also worse.</p>
<p><img src="images/x/badlogicgames-2105234146499203255.png" alt="A message: I'm trying to connect to Figma's remote MCP server. Figma only accepts certain client names during sign-in, e.g., Claude Code or Codex, but Pi 0.99.1 always sends pi." /></p>
<p>— Mario Zechner (@badlogicgames), <a href="https://x.com/badlogicgames/status/2105234146499203255">30 September 2026</a></p>
</blockquote>
</div>
<p>Lovable and Replit turn a prompt into a deployed application. I am trying to cover the rest of the founder’s work as well: operating the product, acquiring users, understanding what they do, and feeding that evidence into the next decision.</p>
<p>The technical foundation is different too. Skills can improve an agent on a JavaScript or Python project, but they cannot change its execution model or the conflicting conventions it learned there. Mine uses Elixir and OTP as the common environment and adds the checks, the observable JavaScript, the shared storage, and the live product data.</p>
<p>Lovable and Replit grew out of venture-capital culture, where rapid user growth can take priority over margins and usage can be subsidized while a company searches for scale. I am building from the perspective of a technical founder spending his own money. Infrastructure cost, API usage, acquisition cost, conversion, and the work required to operate the product are part of the system from the beginning.</p>
<h2 id="where-this-stands">Where this stands<a href="#where-this-stands" aria-label="Link to heading 'Where this stands'" data-heading-content="Where this stands" class="anchor"></a></h2>
<p>The system in this post is still being built; nothing installs it with one command yet. Most of the building blocks are public and useful on their own; the map near the top marks which are still alpha or beta.</p>
<p>The main missing piece is integration. These projects already use one another—<a href="https://github.com/elixir-volt/volt">Volt</a> uses <a href="https://github.com/elixir-volt/quickbeam">QuickBEAM</a>, <a href="https://github.com/elixir-vibe/exograph">Exograph</a> uses <a href="https://github.com/elixir-vibe/quackdb">QuackDB</a> and <a href="https://github.com/elixir-vibe/ex_ast">ExAST</a>, <a href="https://github.com/elixir-vibe/host_kit">HostKit</a> consumes <a href="https://github.com/elixir-vibe/release_kit">ReleaseKit</a> artifacts, and my projects run the quality tools on themselves—but they do not yet form one founder-facing product. The next phase is connecting what exists.</p>
<p>The most immediate product is OpenPencil Cloud: optional workspaces, synchronization, sharing, comments, and team component libraries, self-hostable, with local files still first-class. I also want to turn reference collection, alternative drafts, comparison, and iterative work with agents acting as designers into one coherent <a href="https://github.com/open-pencil/open-pencil">OpenPencil</a> workflow, backed by a service that provides common access to language and vision models, image generation, and vectorization, something like <a href="https://openrouter.ai">OpenRouter</a> focused on design, while still supporting users’ own credentials.</p>
<p>The path through <a href="https://github.com/dannote/vue-pencil">VuePencil</a>, the coding environment, and the rest of the platform then has to become a product. That includes the conventional building blocks founders currently get from <a href="https://supabase.com">Supabase</a> or libraries such as <a href="https://www.better-auth.com">Better Auth</a>: authentication, accounts, files, notifications, and billing. Most of it will come from existing projects behind coherent Elixir APIs and defaults. And <a href="https://github.com/elixir-vibe/incant">Incant</a> has to connect to the operational sources that already exist.</p>
<p>Some packages will merge, some abstractions will change after real use, and some experiments will fail. Publishing the pieces independently is how I find out which.</p>
<h2 id="how-to-help">How to help<a href="#how-to-help" aria-label="Link to heading 'How to help'" data-heading-content="How to help" class="anchor"></a></h2>
<p>Agents let me cover a lot of ground alone, but they cannot provide the variety of real use that a community can. Publishing each building block separately puts it in front of files, operating systems, and workflows I never tested. Bug reports expose my assumptions, and contributions take the projects beyond my own needs. Open source is how I develop the platform as much as how I distribute it.</p>
<p>So the best way to help is to use the projects in situations I have not considered, report what breaks, and tell me which parts are useful outside your own workflow. Contributions are welcome too.</p>
<h2 id="funding-the-next-phase">Funding the next phase<a href="#funding-the-next-phase" aria-label="Link to heading 'Funding the next phase'" data-heading-content="Funding the next phase" class="anchor"></a></h2>
<p>OpenPencil itself stays free and open source, and OpenPencil Cloud will be self-hostable. What I intend to build a company on is the services around it: hosted storage and collaboration, design references and other context for agents, illustration generation, and a design-API service, some of them in partnership with providers that already do this well.</p>
<p>Those services do not exist yet. Building them takes time, and most of the work runs on agents, so model tokens are a cost of their own. Until the services carry the work, I am looking for support.</p>
<p>&lt;.article_callout&gt;</p>
<!-- raw HTML omitted -->]]></content></entry><entry><title>After trying Astra</title><id>https://dannote.net/writing/after-trying-astra/</id><updated>2026-09-10T00:00:00Z</updated><published>2026-09-10T00:00:00Z</published><link href="https://dannote.net/writing/after-trying-astra/"/><summary>Long-term direction, conversational boundaries, and the limits of current models.</summary><content type="html"><![CDATA[<p>When Astra came out, looking at all the cool demos, I started to think that I had to review and rethink the whole concept behind the projects I’ve been working on.</p>
<p>After using it for a few days, I can conclude that while it is significant progress, there is still nothing paradigmatically new. It looks like we’ve plateaued across a whole set of cognitive abilities.</p>
<p>First, Astra, like other models, is still not critical enough of itself. It relies heavily on previous context compactions, doesn’t stop to take a bird’s-eye view of what has been done, and therefore isn’t capable of maintaining and updating a long-term vision.</p>
<p>This isn’t about having a checklist in a Markdown file. Sometimes you have to stop, reflect on the direction you’re going in, and make corrections with some degree of autonomous will.</p>
<p>Second, it still isn’t able to maintain strict boundaries within a conversation. I often ask models to prepare a draft of a reply to a PR, and they consistently fail either by leaking details from the internal conversation or by stepping out of my role and adding things like “X tests passed.”</p>
<p>This isn’t specific to Astra. All frontier models are still very bad at separating the conversational context from what should be retained in the code or shared with a third party, as well as what should remain private and what each party is already aware of. <a href="https://x.com/zack_overflow/status/2076361674014015600">Zack noticed</a> the same thing with Fable back in July: code comments that reference details from the chat and make little sense to anyone reading the codebase.</p>
<p>Of course, this can be tuned with a long prompt containing a set of example situations and appropriate replies, but that would still be imitation rather than understanding.</p>
<p>Third, long-tail sampling artifacts aren’t going anywhere either. Astra can still occasionally stop abruptly, mix up parts of words, or inject random fragments into otherwise coherent output. Once that happens, it won’t be able to recover on its own.</p>
<p>So it’s not AGI yet. Not even close.</p>
<p>Learning how to answer questions and learning how to code, no matter how complex the resulting code is, still doesn’t expose the model during training to the whole variety of behaviors that are natural for human beings.</p>
<p>So my approach is still valid: build environments where existing model capabilities, with some human guidance, can be used to collect enough data to train different behaviors. That’s how <code>figma-use</code>, and later OpenPencil, started.</p>]]></content></entry><entry><title>JavaScript as BEAM code</title><id>https://dannote.net/writing/javascript-as-beam-code/</id><updated>2026-08-14T00:00:00Z</updated><published>2026-08-14T00:00:00Z</published><link href="https://dannote.net/writing/javascript-as-beam-code/"/><summary>QuickBEAM 0.11 adds a JavaScript interpreter written in Elixir, so the BEAM can preempt, limit, and supervise it.</summary><content type="html"><![CDATA[<p><a href="https://github.com/elixir-volt/quickbeam/releases/tag/v0.11.0">QuickBEAM 0.11</a> includes an experimental JavaScript interpreter written in Elixir.</p>
<p>JavaScript is compiled with QuickJS, verified, and can be pinned at startup. Each execution gets fresh state, preventing data from leaking between requests or jobs.</p>
<p>Because execution happens as BEAM code, Erlang can preempt and account for it like any other process. Under concurrent load, this should provide fairer scheduling and more predictable latency than work hidden behind a native runtime boundary.</p>
<p>Step, stack, memory, and wall-clock limits contain failures to a single evaluation.</p>
<p>This brings BEAM scheduling, isolation, and supervision to SSR, templates, user scripts, and small edge-style functions. For now, it supports a limited, tested JavaScript profile rather than the full browser or Node.js environment.</p>
<p>The <a href="/writing/a-javascript-runtime-in-the-beam/">original QuickBEAM runtime</a> still runs each JavaScript runtime on its own OS thread; the interpreter is for work that should live under the BEAM scheduler instead.</p>]]></content></entry><entry><title>Failures of the environment</title><id>https://dannote.net/writing/what-coding-agents-need/</id><updated>2026-07-02T00:00:00Z</updated><published>2026-07-02T00:00:00Z</published><link href="https://dannote.net/writing/what-coding-agents-need/"/><summary>Reading existing code, understanding architecture, and checking for drift.</summary><content type="html"><![CDATA[<div class="x-post">
<blockquote>
<p>Claude Code is vibecoded and full of spyware, it's possible Anthropic doesn't even know what's in there. After reading this report, we are banning it from our systems and strongly encourage other enterprises to do the same. It is an unacceptable security risk.</p>
<p>— the tiny corp (@__tinygrad__), <a href="https://x.com/__tinygrad__/status/2072091861473431605">30 June 2026</a></p>
</blockquote>
</div>
<p>Finally, somebody said it out loud. I’m not proud of OpenPencil’s code quality, but somehow a project maintained by a single developer is in better shape than what I saw in the leaked Claude Code source.</p>
<p>That’s also the primary reason I’m very skeptical about coding-agent swarms burning zillions of tokens. I don’t mean to insult anyone, but the last time I looked at OpenClaw’s code, it was in pretty bad shape.</p>
<p>There are three fundamental things you need to do constantly in your so-called loops if you want to keep a codebase healthy.</p>
<p>First, you need to force the agent to read the existing code to understand your style implicitly. Agents are tuned to read less and more selectively to save space in the context window. This is especially bad in JS codebases, where historically there have been too many coding styles across the ecosystem. Unless you show the agent examples of your style, expect a dice roll every time.</p>
<p>Second, you need to remind the agent to look around. It should be able to inspect the surrounding architectural slice to learn what entities already exist, how they relate to each other, and how the source tree is organized. Otherwise it will tend to satisfy the “business need” by taking the shortest path, like the laziest junior developer. It will likely introduce duplicate entities and multiple implementations of the same concepts, then evolve them independently, making the situation worse over time. It will reimplement functionality that already exists in the standard library or the ecosystem, create ad hoc test files, favor loosely connected internal abstractions, and later spend a lot of effort “normalizing” data between them. Over time, the codebase will gradually drift away from the architecture you originally built.</p>
<p>Third, if you don’t want to waste your time babysitting the agent and repeatedly enforcing best practices, you need to give it the tools. Prompts, skills, or whatever you want to call them, will never fully override the model’s innate behavioral patterns. You need an automated oracle that continuously checks the output for style, code duplication, antipatterns, and architectural drift.</p>
<p>A simple linter setup won’t help much, because most linters are still designed to catch human mistakes. Agents make those mistakes too, but they also introduce an entirely new class of problems on top of them.</p>
<p>None of this is really the agent’s fault. These are failures of the environment, not the model itself. Give it the right hints, the right context, and the right tools, and it can avoid most of these problems and write the code properly. The real problem isn’t the models anymore. They’re already pretty strong. The real problem is the setup around them.</p>
<p>Given that barely any of this has been seriously addressed by the coding harnesses from the major AI labs, I have the feeling that very few of them actually care about what it takes to build and maintain high-quality software.</p>
<p>Claude Code itself is proof of that.</p>]]></content></entry><entry><title>Writing is the benchmark</title><id>https://dannote.net/writing/writing-is-the-benchmark/</id><updated>2026-06-11T00:00:00Z</updated><published>2026-06-11T00:00:00Z</published><link href="https://dannote.net/writing/writing-is-the-benchmark/"/><summary>Models built to generate text are still bad at writing it, and continuing someone else’s fiction shows why.</summary><content type="html"><![CDATA[<div class="x-post">
<blockquote>
<p>You should basically never use Fable for coding, but instead use it as a planner/orchestrator.</p>
<p>Most of today's advanced models can implement a spec perfectly, and once done you can send the work to Fable to review.</p>
<p>This has been my most powerful flow so far.</p>
<p>— Pietro Schirano (@skirano), <a href="https://x.com/skirano/status/2065096311410409770">11 June 2026</a></p>
</blockquote>
</div>
<p>For me, the ultimate intelligence benchmark is still text writing. Which is kind of ironic, because models that were initially created to generate text are still often that bad at it.</p>
<p>If you write professionally, you quickly notice that it’s usually easier to write the text yourself than to edit whatever the model generated. Today, only the largest models can be genuinely helpful with that. That’s GPT-5.5 Pro — which, judging by its behavior, feels to me like a successor to the sunsetted 4.1 large model — and, as of today, Fable too.</p>
<p>My benchmark is pretty simple: I take large portions of fiction books by no-name authors, including me, and ask the model to continue them. Only the larger models are really able to capture the voice, the barely noticeable nuances of vocabulary, and the author’s biases. Smaller models quickly fall back into default, flavorless narration, with a few bits of pretentiousness sprinkled in.</p>]]></content></entry><entry><title>Let the agent keep the plan</title><id>https://dannote.net/writing/talking-to-agents-in-shorthand/</id><updated>2026-06-10T00:00:00Z</updated><published>2026-06-10T00:00:00Z</published><link href="https://dannote.net/writing/talking-to-agents-in-shorthand/"/><summary>Predictable habits, a plan the model keeps up to date, and prompts that keep getting shorter.</summary><content type="html"><![CDATA[<div class="x-post">
<blockquote>
<p>On the matter of loops, I give you my 12 months old slop blog post. Let's call it a &quot;proto-loop&quot;. We were much more innocent back then.</p>
<p><a href="https://mariozechner.at/posts/2025-06-02-prompts-are-code/">https://mariozechner.at/posts/2025-06-02-prompts-are-code/</a></p>
<p>— Mario Zechner (@badlogicgames), <a href="https://x.com/badlogicgames/status/2064491848018587897">9 June 2026</a></p>
</blockquote>
</div>
<p>I discovered that I’m (unsurprisingly) a very predictable person over longer periods of time.</p>
<p>So I pretend I don’t know what to do next and ask the agent every time. It updates the plan, and I mostly make small adjustments along the way.</p>
<p>Of course, sometimes I have to stop and write a lot of profanity. But most of the time it works surprisingly well and even suggests ideas I wouldn’t have arrived at as quickly myself.</p>
<p>The funny part is that models like GPT-5.5 are already good enough at understanding my habits that I can communicate in increasingly caveman-like language. I have prompt-template shortcuts for many common interactions, and I keep making them shorter and more cryptic. These days I often type just a few letters and let the rest unfold from there.</p>
<p>I’m gradually formalizing the process and will probably write about it soon.</p>]]></content></entry><entry><title>Why DuckDB</title><id>https://dannote.net/writing/why-duckdb/</id><updated>2026-06-07T00:00:00Z</updated><published>2026-06-07T00:00:00Z</published><link href="https://dannote.net/writing/why-duckdb/"/><summary>Where DuckDB fits alongside Postgres, and what QuackDB adds for Elixir.</summary><content type="html"><![CDATA[<p>I’m increasingly convinced that <a href="https://duckdb.org">DuckDB</a> is becoming one of the best storage choices for hobby to medium-sized projects.</p>
<p>Postgres is still the obvious default for highly concurrent OLTP systems, but many small and medium products are read-heavy, append-heavy, analytical, or operated by a small team. For those workloads, DuckDB’s columnar execution model, vectorized engine, rich SQL surface, and embedded/local-first deployment model can be a much better fit.</p>
<p>I intend to use DuckDB as the primary storage layer for the platform I’m building, so QuackDB focuses on the parts I need in a real Elixir app: supervision, connection pooling, Ecto, fast append paths, dataframes, telemetry, and helpers for DuckDB’s analytical SQL.</p>
<p><a href="https://github.com/elixir-vibe/quackdb/releases/tag/v0.5.0">QuackDB 0.5</a> focuses on making the Ecto and analytical side much more complete:</p>
<ul>
<li>append through Ecto with defaults and returning</li>
<li>using an Ecto repo directly with QuackDB native query/append APIs</li>
<li>advanced join patterns, including semi/anti and <code>ASOF</code>-style queries</li>
<li>DuckDB star/<code>COLUMNS</code> expressions in SQL and Ecto</li>
<li>broader <code>LIST</code>/<code>MAP</code>/<code>STRUCT</code> helpers</li>
<li><code>PIVOT</code>, <code>UNPIVOT</code>, <code>GROUPING SETS</code>, <code>ROLLUP</code>, and <code>CUBE</code> builders</li>
<li><code>LIST</code> lambdas in Ecto with <code>fn</code> syntax and <code>case_when</code></li>
<li>better nullable/schema type handling for append-heavy workloads</li>
</ul>
<p>QuackDB also continues to cover the broader integration surface: supervised DuckDB, DBConnection/Ecto, native append, Explorer dataframes, Table.Reader results, Geo/WKB, telemetry, managed DuckDB binaries, and DuckDB-specific SQL helpers.</p>
<h2 id="update-august-2026">Update, August 2026<a href="#update-august-2026" aria-label="Link to heading 'Update, August 2026'" data-heading-content="Update, August 2026" class="anchor"></a></h2>
<p>DuckLabs <a href="https://x.com/duckdb/status/2092598676439044577">is joining AWS</a>, with DuckDB and the rest of the Duck Stack staying MIT-licensed under the DuckDB Foundation. I’m now pretty confident that DuckDB is the right bet as the primary storage engine for the platform I’m building.</p>
<p>QuackDB, the Ecto adapter for DuckDB’s Quack protocol, is in good shape. I’ve spent a lot of effort polishing the DSL and internals for scenarios like bulk inserts, and I’m already using it in production across several projects.</p>]]></content></entry><entry><title>Forty packages, one maintainer</title><id>https://dannote.net/writing/forty-packages-one-maintainer/</id><updated>2026-06-06T00:00:00Z</updated><published>2026-06-06T00:00:00Z</published><link href="https://dannote.net/writing/forty-packages-one-maintainer/"/><summary>How I handle pull requests when AI makes them cheap to open and expensive to review.</summary><content type="html"><![CDATA[<p>Since the beginning of the year, the number of projects I actively maintain across several ecosystems has grown to more than 40 packages.</p>
<p>I do my best to respond quickly and help with adoption. I’ve done my best to extract signal even from the worst PRs. But my capacity is limited, so here is how I handle pull requests in these dark times.</p>
<p>If a PR is well-written overall but I only have a few nitpicks, I often merge it and make a follow-up commit myself. The review back-and-forth is often more expensive, and it’s harder to keep track of many conversations.</p>
<p>If a PR isn’t in great shape and I’m not willing to merge it, I treat it as an open issue instead. If it contains good ideas or useful hints on how to fix things, I do a “vibe-merge”: I implement the code myself but add the original author as a co-author to give proper credit.</p>
<p>Otherwise, I close it with a polite AI-generated reply.</p>
<p>Many people are saying that OpenPencil has one of the most generous pull request policies anywhere in the world. Frankly, they’re probably right. So pull requests that don’t follow <a href="https://github.com/open-pencil/open-pencil/blob/master/CONTRIBUTING.md">CONTRIBUTING.md</a> are now rejected automatically.</p>]]></content></entry><entry><title>Where Figma Make will get stuck</title><id>https://dannote.net/writing/where-figma-make-will-get-stuck/</id><updated>2026-05-30T00:00:00Z</updated><published>2026-05-30T00:00:00Z</published><link href="https://dannote.net/writing/where-figma-make-will-get-stuck/"/><summary>Design-to-code needs abstractions that map both ways, not generated backends nobody can maintain.</summary><content type="html"><![CDATA[<p>In my opinion, that’s the point where they’ll get stuck.</p>
<p>The next step requires proper abstractions for bidirectional design-to-code mapping.</p>
<p>And all the vibe-coding platforms like Replit and Lovable are already hitting the limits of project scale because they generate unmanaged and unmaintainable AI-slop backends.</p>
<p>That’s why I feel comfortable building a competitor to all of them, solo and in the open.</p>
<div class="x-post">
<blockquote>
<p>New capabilities coming to Figma Make (Rolling out now in our Beta desktop app)</p>
<p>→ Bring your codebase to Make<br />
→ Edit code with precise changes<br />
→ Branch, commit, ship</p>
<p>— Figma (@figma), <a href="https://x.com/figma/status/2060099693464478054">28 May 2026</a></p>
</blockquote>
</div>]]></content></entry><entry><title>A coding agent built on OTP</title><id>https://dannote.net/writing/a-coding-agent-built-on-otp/</id><updated>2026-05-22T00:00:00Z</updated><published>2026-05-22T00:00:00Z</published><link href="https://dannote.net/writing/a-coding-agent-built-on-otp/"/><summary>Vibe puts Elixir eval, stateful sessions, and supervised subagents at the center of a coding agent.</summary><content type="html"><![CDATA[<p>A lot of people expressed interest, so I decided to open-source <a href="https://github.com/elixir-vibe/vibe">Vibe</a> earlier than planned.</p>
<p>Vibe combines the vibe-coding tools I’ve been building into an Elixir-focused coding agent. But it is not only about coding: the goal is to support background tasks, long-running agent workflows, and access through Telegram and other gateways.</p>
<p>Some highlights:</p>
<ul>
<li><strong>Elixir eval is the primary tool interface.</strong> This makes it possible to unify many actions behind one composable interface, in a Unix-like way, without constant serialization churn.</li>
<li><strong>Sessions are stateful.</strong> Similar to Livebook, the agent can store and reference intermediate results naturally.</li>
<li><strong>It is built on OTP.</strong> Unlike most agent harnesses, Vibe can deeply introspect and supervise its own internal state.</li>
<li><strong>Agents can launch other agents.</strong> Subagents are supervised OTP processes with their own sessions, which enables more sophisticated long-running and parallel workflows.</li>
<li><strong>Agents can reach other machines.</strong> Vibe supports remote access over SSH and Erlang distribution, so agents can connect to remote nodes and communicate across machines.</li>
<li><strong>The agent can work on itself.</strong> It can inspect, patch, verify, and hot-reload parts of Vibe, while tools like <a href="https://github.com/elixir-vibe/reach">Reach</a> help keep boundaries clean.</li>
</ul>
<p>Around that, there is a TUI, a LiveView web interface, persistent sessions, SQLite-backed storage, semantic events, plugins, skills, gateways, and local telemetry.</p>
<p>This is still an early research preview. Use it at your own risk, expect rough edges, and be careful.</p>]]></content></entry><entry><title>The cleanup cycle</title><id>https://dannote.net/writing/a-moment-of-truth/</id><updated>2026-05-21T00:00:00Z</updated><published>2026-05-21T00:00:00Z</published><link href="https://dannote.net/writing/a-moment-of-truth/"/><summary>On imitation of competence, hiring rituals, and the cleanup cycle that follows.</summary><content type="html"><![CDATA[<div class="x-post">
<blockquote>
<p>Today we reduced headcount by 22%. The business is the strongest it's ever been. So I think it's important to be direct about what I'm seeing and why.</p>
<p>First, I made this decision and I own it. I did it because the way to operate at the highest level of productivity is changing, and to win the future, ClickUp needs to change with it. …</p>
<p>— Zeb Evans (@DJ_CURFEW), <a href="https://x.com/DJ_CURFEW/status/2057522382315929802">21 May 2026</a></p>
</blockquote>
</div>
<p>ClickUp just cut 22% of its staff while saying the business has never been stronger. Honestly, this feels like a moment of truth for the industry.</p>
<p>Software development, originally an engineering discipline, has recently been flooded with impostors lacking both skills and education. At its peak, companies started hiring people after just a few months of online courses. Many of them learned only how to imitate competence by repeating corporate jargon and trendy buzzwords.</p>
<p>I’ve personally interviewed dozens of so-called frontend developers who had no idea how computers actually work. I’ve also interviewed so-called DevOps engineers who couldn’t even explain the OSI model. And over time we were gradually gaslighted into believing that this is simply the “new division of labor”. Once the AI wave started, it became even worse — people began cheating on interviews like crazy.</p>
<p>Poor leadership turned programming into something closer to management consulting, filled with endless meetings and rituals.</p>
<p>Today feels like the beginning of a cleanup cycle.</p>]]></content></entry><entry><title>A language for humans and models</title><id>https://dannote.net/writing/a-language-for-humans-and-models/</id><updated>2026-05-01T00:00:00Z</updated><published>2026-05-01T00:00:00Z</published><link href="https://dannote.net/writing/a-language-for-humans-and-models/"/><summary>JSX, structured JSON, and Bash are steps toward one declarative language understood by humans and models alike.</summary><content type="html"><![CDATA[<p>If you think a bit deeper about what <a href="https://www.remotion.dev">Remotion</a> is doing, and what I was doing when I chose JSX as the primary language for <a href="https://github.com/dannote/figma-use"><code>figma-use</code></a> templates and OpenPencil templates for CLI and MCP, it’s the same idea. We’ve all been searching for a universal language that is understandable to both LLMs and humans, and expressive enough to describe sophisticated ideas declaratively.</p>
<p>Plain text isn’t enough. You can see hints of this in models like Nano Banana, where users discovered that well-structured JSON produces far more deterministic and higher-quality results.</p>
<p>Coding agents, which mostly express their intent through compact and concise Bash commands, are also a step in that direction.</p>
<p>My intuition is that Elixir, with its expressive syntax and first-class metaprogramming, is the next step.</p>]]></content></entry><entry><title>Approximating behavior</title><id>https://dannote.net/writing/approximating-behavior/</id><updated>2026-05-01T00:00:00Z</updated><published>2026-05-01T00:00:00Z</published><link href="https://dannote.net/writing/approximating-behavior/"/><summary>Models learn the most from acting in constrained environments, which makes feedback on their actions the valuable data.</summary><content type="html"><![CDATA[<p>Language models started as next-token predictors, and lately they have been drifting toward approximating behavior.</p>
<p>The general knowledge they absorbed from sources like Common Crawl isn’t intelligence, not even a real approximation of it. We started seeing early signs of intelligence when we placed models in constrained environments like coding harnesses and made them act: change a file, run the tests, read the error, try again.</p>
<p>That loop is what I keep building for other domains. <a href="https://github.com/dannote/figma-use"><code>figma-use</code></a> gives a design agent the cycle a coding agent already has: it edits the actual document, sees a visual diff of what changed, and runs a design linter before deciding what to do next. <a href="https://github.com/elixir-vibe/reach">Reach</a> turns an architecture into rules an agent can check, so a change that crosses a boundary fails instead of slipping through review.</p>
<p>A harness like this does two things. It makes today’s models more useful, and it produces data no crawl contains: what an agent tried in a specialized environment, what the environment answered, and what a human expert accepted. That kind of data can’t be collected from static sources, which is how harnesses help break through the limits model producers face.</p>
<p>So the scarce resource is high-quality feedback on actions. When I build tools for vibe-coding, I also think of them as a way to validate synthetic datasets for future models. If a harness can reliably tell a better action from a worse one, the better actions can be kept and learned from, and some of what we now get from the harness can become part of the model itself.</p>]]></content></entry><entry><title>A JavaScript runtime in the BEAM</title><id>https://dannote.net/writing/a-javascript-runtime-in-the-beam/</id><updated>2026-03-12T00:00:00Z</updated><published>2026-03-12T00:00:00Z</published><link href="https://dannote.net/writing/a-javascript-runtime-in-the-beam/"/><summary>QuickBEAM gives every JavaScript runtime a supervision tree, native BEAM terms, and no Node.js on the machine.</summary><content type="html"><![CDATA[<p>I've built a new JavaScript runtime that runs inside the BEAM.</p>
<p>Every JS runtime is a GenServer with its own OS thread. No JSON anywhere — JS objects map to BEAM terms natively through a lock-free queue.</p>
<p>What makes it different from running Node/Deno/Bun alongside Elixir:</p>
<ul>
<li>JS runtimes live in supervision trees. They crash, restart, recover state — standard OTP</li>
<li><code>fetch()</code> goes through <code>:httpc</code>. WebSocket through <code>:gun</code>. <code>crypto.subtle</code> through <code>:crypto</code>. <code>BroadcastChannel</code> through <code>:pg</code> — works across a cluster</li>
<li>The DOM is lexbor (C library). JS renders into it, Elixir reads it directly — no serialization, no re-parsing</li>
<li>Workers are BEAM processes. They get preemptive scheduling for free</li>
<li>TypeScript toolchain (OXC) and npm client built in — no Node.js on the machine at all</li>
</ul>
<p>Full control over the JS layer: parse ASTs, bundle imports, transform TypeScript, minify — all from Elixir via OXC NIFs.</p>
<p>Use cases:</p>
<ul>
<li>SSR with Preact/React into native DOM, Elixir reads the tree</li>
<li>Sandboxed user-defined business rules with memory limits, timeouts, and a controlled API surface</li>
<li>Parallel Workers that compute and broadcast via distributed process groups</li>
<li>Evaluating or bundling TypeScript without any external toolchain</li>
<li>Running npm packages inside the BEAM</li>
</ul>
<p>Still a research project in early beta. Covered with tests including Web Platform Tests ports, but expect rough edges.</p>
<p>&lt;.article_link_card href=&quot;https://github.com/elixir-volt/quickbeam&quot; title=&quot;elixir-volt/quickbeam&quot; description=&quot;JavaScript runtime for the BEAM: Web APIs backed by OTP, native DOM, no Node.js.&quot; /&gt;</p>]]></content></entry><entry><title>Three days, one developer</title><id>https://dannote.net/writing/three-days-one-developer/</id><updated>2026-03-01T00:00:00Z</updated><published>2026-03-01T00:00:00Z</published><link href="https://dannote.net/writing/three-days-one-developer/"/><summary>Figma shipped a silent patch to kill figma-use, so I recreated the core of it as OpenPencil in a weekend.</summary><content type="html"><![CDATA[<p>Figma shipped a silent patch specifically to kill <a href="https://github.com/dannote/figma-use"><code>figma-use</code></a> — my open-source tool that did what they wouldn't: an MCP server that creates and modifies designs, JSX export, design linting. Then they scrambled to catch up with their own MCP server.</p>
<p>So I spent the weekend recreating Figma from scratch.</p>
<p>OpenPencil: reads and writes <code>.fig</code> files, AI chat with full design tools, P2P collaboration with zero servers, ~7 MB app. No account, no subscription.</p>
<p>Three days, one developer, MIT license.</p>
<p>&lt;.article_link_card href=&quot;https://openpencil.dev&quot; title=&quot;OpenPencil&quot; description=&quot;A free, open-source design editor that reads and writes .fig files. Desktop and web.&quot; /&gt;</p>]]></content></entry></feed>