<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: André Ahlert</title>
    <description>The latest articles on Forem by André Ahlert (@andreahlert).</description>
    <link>https://forem.com/andreahlert</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3824067%2Fe332f5fe-20ae-4eb9-9f99-ccb3befeb2ad.png</url>
      <title>Forem: André Ahlert</title>
      <link>https://forem.com/andreahlert</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/andreahlert"/>
    <language>en</language>
    <item>
      <title>Soda Moved to ELv2. Provero Is Apache 2.0.</title>
      <dc:creator>André Ahlert</dc:creator>
      <pubDate>Wed, 01 Apr 2026 14:19:06 +0000</pubDate>
      <link>https://forem.com/andreahlert/soda-moved-to-elv2-provero-is-apache-20-42l9</link>
      <guid>https://forem.com/andreahlert/soda-moved-to-elv2-provero-is-apache-20-42l9</guid>
      <description>&lt;p&gt;When Soda changed its license from Apache 2.0 to Elastic License v2, teams that relied on Soda Core as open source infrastructure had to re-evaluate. This post explains what changed, what it means for you, and what alternatives exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;Soda Core was originally released under Apache License 2.0. In 2023, Soda switched to the Elastic License v2 (ELv2). The change applied to all new versions of Soda Core and its associated packages.&lt;/p&gt;

&lt;p&gt;ELv2 is not an open source license by the OSI definition. It adds two restrictions that Apache 2.0 does not have: you cannot offer the software as a managed service, and you cannot modify the license key functionality. For internal use at most companies, ELv2 is permissive enough. But for platform vendors, consultancies embedding Soda in their products, or organizations with strict open source policies, it creates friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who is affected
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Internal data teams&lt;/strong&gt; (Low impact) -- ELv2 allows internal use. You can keep using Soda Core if the license terms work for your legal team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data platform vendors&lt;/strong&gt; (High impact) -- If you embed data quality checks in a product you sell, ELv2 prohibits offering it as a managed service without a commercial agreement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consultancies and integrators&lt;/strong&gt; (Medium impact) -- Depends on how you distribute. If you ship Soda as part of a client deployment, review the license terms with legal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open source projects&lt;/strong&gt; (High impact) -- ELv2 is not OSI-approved. If your project requires OSI-approved dependencies, you cannot depend on Soda Core.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is a pattern, not an exception
&lt;/h2&gt;

&lt;p&gt;Soda is not the first data tool to make this move. The playbook is familiar across the industry: release as open source, build adoption, then change the license to protect a commercial offering. Elastic did it. MongoDB did it. HashiCorp did it. Each time, the community had to decide whether to accept the new terms, fork the project, or find an alternative.&lt;/p&gt;

&lt;p&gt;The pattern is rational from a business perspective. But it breaks trust with teams who built infrastructure on the assumption that the license would not change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Provero does differently
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/provero-org/provero" rel="noopener noreferrer"&gt;Provero&lt;/a&gt; is licensed under Apache 2.0. Every feature ships in the open source package: anomaly detection, data contracts, all 16 check types, the CLI, the Airflow provider. There is no cloud-only tier and no feature gating.&lt;/p&gt;

&lt;p&gt;We are pursuing acceptance into the LF AI &amp;amp; Data Foundation, which means the project would be governed by a neutral foundation, not a single company. Foundation governance makes unilateral license changes structurally difficult.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Provero&lt;/th&gt;
&lt;th&gt;Soda Core&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;ELv2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OSI approved&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed service allowed&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anomaly detection&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;Cloud only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data contracts&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;Partial (Cloud for full)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;Targeting LF AI Foundation&lt;/td&gt;
&lt;td&gt;Soda Inc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check format&lt;/td&gt;
&lt;td&gt;YAML&lt;/td&gt;
&lt;td&gt;SodaCL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migration path&lt;/td&gt;
&lt;td&gt;&lt;code&gt;provero import soda&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;If you have existing SodaCL checks, Provero includes a converter that maps Soda check syntax to Provero YAML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;provero
provero import soda checks.yaml &lt;span class="nt"&gt;-o&lt;/span&gt; provero.yaml
provero run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SodaCL&lt;/th&gt;
&lt;th&gt;Provero&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;missing_count(col) = 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;not_null: col&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;duplicate_count(col) = 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;unique: col&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;row_count &amp;gt; 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;row_count: { min: 1 }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;freshness(col) &amp;lt; 24h&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;freshness: { column: col, max_age: 24h }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;valid_count(col) = ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;accepted_values: { column: col, values: [...] }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Checks that don't have a direct equivalent are preserved as YAML comments, so nothing is silently dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our position on licensing
&lt;/h2&gt;

&lt;p&gt;We think data quality is infrastructure. It belongs in the same category as linters, test frameworks, and CI tools. You would not accept a linter that moved half its rules behind a paywall. Data quality checks should work the same way: open, portable, composable.&lt;/p&gt;

&lt;p&gt;Provero will stay Apache 2.0. Not because we are against commercial models, but because we believe the right way to build a business around open source is to sell services, hosting, and support on top of a fully open core. Not to restrict the core itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;provero
provero init
provero run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/provero-org/provero" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://pypi.org/project/provero/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt; | &lt;a href="https://provero-org.github.io/provero/" rel="noopener noreferrer"&gt;Docs&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>dataengineering</category>
      <category>devops</category>
    </item>
    <item>
      <title>I built a backend language that a 3B model writes better than Express</title>
      <dc:creator>André Ahlert</dc:creator>
      <pubDate>Wed, 01 Apr 2026 10:40:12 +0000</pubDate>
      <link>https://forem.com/andreahlert/i-built-a-backend-language-that-a-3b-model-writes-better-than-express-2im3</link>
      <guid>https://forem.com/andreahlert/i-built-a-backend-language-that-a-3b-model-writes-better-than-express-2im3</guid>
      <description>&lt;p&gt;I've been building web apps for years and the thing that always bothered me is how much ceremony goes into something that should be simple. A task list with auth shouldn't need 15 files across 3 directories with 200 lines of config.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/kilnx-org/kilnx" rel="noopener noreferrer"&gt;Kilnx&lt;/a&gt;, a declarative backend language. 27 keywords, compiles to a single binary, SQL inline, HTML as output. At some point I started wondering: if the language is this small, can a tiny local LLM write it? A model that fits on a phone?&lt;/p&gt;

&lt;p&gt;I ran the benchmark. Kilnx won every round.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Kilnx looks like
&lt;/h2&gt;

&lt;p&gt;A complete app with auth, pagination, htmx, and a SQLite database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config
  database: "sqlite://app.db"
  port: 8080
  secret: env SECRET_KEY required

model user
  name: text required
  email: email unique
  password: password required

model task
  title: text required
  done: bool default false
  owner: user required
  created: timestamp auto

auth
  table: user
  identity: email
  password: password
  login: /login
  after login: /tasks

page /tasks requires auth
  query tasks: SELECT id, title, done FROM task
               WHERE owner = :current_user.id
               ORDER BY created DESC paginate 20
  html
    {{each tasks}}
    &amp;lt;tr&amp;gt;
      &amp;lt;td&amp;gt;{title}&amp;lt;/td&amp;gt;
      &amp;lt;td&amp;gt;{{if done}}Yes{{end}}&amp;lt;/td&amp;gt;
      &amp;lt;td&amp;gt;
        &amp;lt;button hx-post="/tasks/{id}/delete"
                hx-target="closest tr"
                hx-swap="outerHTML"&amp;gt;Delete&amp;lt;/button&amp;gt;
      &amp;lt;/td&amp;gt;
    &amp;lt;/tr&amp;gt;
    {{end}}

action /tasks/create method POST requires auth
  validate task
  query: INSERT INTO task (title, owner)
         VALUES (:title, :current_user.id)
  redirect /tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;kilnx build app.kilnx -o myapp&lt;/code&gt; gives you a ~15MB binary. Registration, login with bcrypt, sessions, CSRF, validation, pagination, htmx inline delete. No framework, no ORM, no node_modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;The Kilnx grammar fits in 400 lines of docs. Express, Django, and Node.js each have thousands of pages of documentation, dozens of APIs, and multiple ways to do the same thing.&lt;/p&gt;

&lt;p&gt;I wanted to know if that difference in surface area shows up when you ask small LLMs to generate code. Not GPT-4 or Claude, but models you run on a laptop with Ollama. Models between 1B and 7B parameters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;I wrote 10 equivalent tasks across four stacks (Kilnx, Express, Django, vanilla Node.js):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Difficulty&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Hello World page&lt;/td&gt;
&lt;td&gt;trivial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;User model definition&lt;/td&gt;
&lt;td&gt;easy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Page with database query&lt;/td&gt;
&lt;td&gt;easy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Create with validation&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Auth + protected route&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Delete with htmx response&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;SSE notifications&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Chat websocket&lt;/td&gt;
&lt;td&gt;hard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Stripe webhook&lt;/td&gt;
&lt;td&gt;hard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Complete mini app&lt;/td&gt;
&lt;td&gt;hard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Five models, three families, all local:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Parameters&lt;/th&gt;
&lt;th&gt;Disk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 2.5 7B&lt;/td&gt;
&lt;td&gt;7B&lt;/td&gt;
&lt;td&gt;4.7 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 2.5 3B&lt;/td&gt;
&lt;td&gt;3B&lt;/td&gt;
&lt;td&gt;1.9 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 2.5 1.5B&lt;/td&gt;
&lt;td&gt;1.5B&lt;/td&gt;
&lt;td&gt;986 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phi-4 Mini&lt;/td&gt;
&lt;td&gt;3.8B&lt;/td&gt;
&lt;td&gt;2.5 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.2 1B&lt;/td&gt;
&lt;td&gt;1B&lt;/td&gt;
&lt;td&gt;1.3 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three validation passes on every output:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keyword matching&lt;/strong&gt; - does the code contain the structural elements the task requires?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Syntax check&lt;/strong&gt; - &lt;code&gt;kilnx check&lt;/code&gt; (semantic analysis), &lt;code&gt;node --check&lt;/code&gt;, &lt;code&gt;python compile&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM-as-judge&lt;/strong&gt; - Qwen 7B rating syntax/completeness/correctness/idiom (0-3 each)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every combination ran 3 times. 600 generations, 600 judge evaluations.&lt;/p&gt;

&lt;h3&gt;
  
  
  About fairness
&lt;/h3&gt;

&lt;p&gt;This is important. &lt;strong&gt;Kilnx has never appeared in any training dataset.&lt;/strong&gt; Zero &lt;code&gt;.kilnx&lt;/code&gt; files exist on the internet outside my repo. Express and Django have millions of code examples baked into every LLM's weights.&lt;/p&gt;

&lt;p&gt;I gave the models the Kilnx grammar reference (11.7K chars) as prompt context. Express, Django, and Node got no reference docs because they don't need them.&lt;/p&gt;

&lt;p&gt;If anything, this setup gives the established frameworks a huge advantage. They've been pre-trained on the entire Stack Overflow + GitHub history. Kilnx gets one document.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Structural correctness (keyword score, averaged over 3 runs)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Kilnx&lt;/th&gt;
&lt;th&gt;Express&lt;/th&gt;
&lt;th&gt;Node.js&lt;/th&gt;
&lt;th&gt;Django&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 2.5 7B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;td&gt;93%&lt;/td&gt;
&lt;td&gt;83%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Qwen 2.5 3B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;99%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;td&gt;89%&lt;/td&gt;
&lt;td&gt;87%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 2.5 1.5B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;99%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;td&gt;87%&lt;/td&gt;
&lt;td&gt;74%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phi-4 Mini&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;98%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;td&gt;93%&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.2 1B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;90%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;78%&lt;/td&gt;
&lt;td&gt;77%&lt;/td&gt;
&lt;td&gt;77%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Qwen 3B, a 1.9 GB model, scores 99% on Kilnx, a language it has never encountered. The same model gets 87% on Django, a framework it has seen millions of times during training.&lt;/p&gt;

&lt;p&gt;When you shrink from 7B down to 1B, Kilnx drops 10 points. Node.js drops 16. The simpler grammar holds up better as the model gets dumber.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tokens per task (completion only)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Qwen 7B&lt;/th&gt;
&lt;th&gt;Qwen 3B&lt;/th&gt;
&lt;th&gt;Qwen 1.5B&lt;/th&gt;
&lt;th&gt;Phi-4 Mini&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kilnx&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;105&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;112&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;111&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;95&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Django&lt;/td&gt;
&lt;td&gt;195&lt;/td&gt;
&lt;td&gt;226&lt;/td&gt;
&lt;td&gt;152&lt;/td&gt;
&lt;td&gt;199&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Express&lt;/td&gt;
&lt;td&gt;302&lt;/td&gt;
&lt;td&gt;349&lt;/td&gt;
&lt;td&gt;265&lt;/td&gt;
&lt;td&gt;315&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;347&lt;/td&gt;
&lt;td&gt;381&lt;/td&gt;
&lt;td&gt;333&lt;/td&gt;
&lt;td&gt;490&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;3x fewer tokens than Express/Node. This is not a style difference. It's the same functionality. A chat websocket in Kilnx is ~110 tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;socket /chat/:room requires auth
  on connect
    query: select body, author.name, created from chat_message
           where room = :room
           order by created desc
           limit 50
    send history

  on message
    validate
      body: required max 500
    query: insert into chat_message (body, author, room)
           values (:body, :current_user.id, :room)
    broadcast to :room fragment chat-bubble
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Express version of the same task runs ~420 tokens of socket.io setup, middleware, database calls, and room management.&lt;/p&gt;

&lt;h3&gt;
  
  
  Session economics
&lt;/h3&gt;

&lt;p&gt;Kilnx has a cost that Express doesn't: the grammar reference takes ~3,100 prompt tokens. But that's loaded once per session. The per-task completion cost is what scales.&lt;/p&gt;

&lt;p&gt;Over a real session with Qwen 3B:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tasks&lt;/th&gt;
&lt;th&gt;Kilnx&lt;/th&gt;
&lt;th&gt;Express&lt;/th&gt;
&lt;th&gt;Node.js&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3,269&lt;/td&gt;
&lt;td&gt;464&lt;/td&gt;
&lt;td&gt;501&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4,277&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4,640&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5,010&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5,957&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11,600&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12,525&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8,757&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;23,200&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;25,050&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14,357&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;46,400&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50,100&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Kilnx becomes cheaper than Express at task 9. By the end of a workday (call it 50-100 tasks with a copilot), you've used 71% fewer tokens. If you're paying per token on an API, that's real money. If you're running locally, it's real time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Raw output from the 3B
&lt;/h3&gt;

&lt;p&gt;No editing, no cherry-picking. This is Qwen 2.5 3B (1.9 GB on disk) generating a complete app from scratch, having seen the Kilnx grammar for the first time in the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config
  database: env DATABASE_URL default "sqlite://app.db"
  port: 8080
  secret: env SECRET_KEY required

model task
  title: text required
  done: bool default false
  owner: user required
  created: timestamp auto

auth
  table: user
  identity: email
  password: password
  login: /login
  after login: /tasks

page /tasks layout main requires auth
  query tasks: select id, title, done from task
               where owner = :current_user.id
               order by created desc
               paginate 20
  html
    &amp;lt;input type="search" name="q" placeholder="Search tasks..."
           hx-get="/tasks" hx-trigger="keyup changed delay:300ms"
           hx-target="#task-list"&amp;gt;
    &amp;lt;table id="task-list"&amp;gt;
      &amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;Title&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Done&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;
      {{each tasks}}
      &amp;lt;tr&amp;gt;
        &amp;lt;td&amp;gt;{title}&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;{{if done}}Yes{{end}}&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;&amp;lt;button hx-post="/tasks/{id}/delete"
                    hx-target="closest tr"
                    hx-swap="outerHTML"&amp;gt;Delete&amp;lt;/button&amp;gt;&amp;lt;/td&amp;gt;
      &amp;lt;/tr&amp;gt;
      {{end}}
    &amp;lt;/table&amp;gt;

action /tasks/create method POST requires auth
  validate task
  query: insert into task (title, owner)
         values (:title, :current_user.id)
  redirect /tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auth, pagination, htmx search with debounce, inline delete, form validation. It even added the search input on its own, that wasn't in the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I think this happens
&lt;/h2&gt;

&lt;p&gt;Express forces the model to make a lot of decisions. CommonJS or ESM? Which middleware in what order? Prisma or Sequelize or raw queries? Passport or express-session or JWT? EJS or Pug or Handlebars? Each fork is a place where a small model can pick wrong.&lt;/p&gt;

&lt;p&gt;Kilnx has one way to do each thing. One keyword for auth, one keyword for pages, one for actions. The model doesn't pick between approaches because there's only one approach. The decision space is so small that even a 1B model mostly gets it right.&lt;/p&gt;

&lt;p&gt;I don't think this is unique to Kilnx. Any DSL with a tight, regular grammar would probably show the same pattern. &lt;strong&gt;The surface area of a language directly predicts how well small models can generate it.&lt;/strong&gt; I haven't seen anyone optimize for that yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do with this
&lt;/h2&gt;

&lt;p&gt;If you're an indie dev or a solo founder shipping CRUD apps:&lt;/p&gt;

&lt;p&gt;A 3B model running locally gives you 99% accuracy on Kilnx with no API costs, no internet, no privacy concerns. The 7B hits 100%. You don't need to send your code to OpenAI to get a working backend.&lt;/p&gt;

&lt;p&gt;If you're using a paid API, the 71% token reduction over a session adds up fast. Especially if you're iterating on features all day.&lt;/p&gt;

&lt;p&gt;If you're just curious, the whole language is 27 keywords. You can read &lt;a href="https://github.com/kilnx-org/kilnx/blob/main/GRAMMAR.md" rel="noopener noreferrer"&gt;the grammar&lt;/a&gt; in 10 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/kilnx-org/kilnx/main/install.sh | sh
kilnx run app.kilnx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/kilnx-org/kilnx" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kilnx-org/kilnx/blob/main/GRAMMAR.md" rel="noopener noreferrer"&gt;Grammar reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kilnx-org/kilnx-example-chat" rel="noopener noreferrer"&gt;Slack Alternative example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kilnx-org/kilnx-org/tree/main/paper" rel="noopener noreferrer"&gt;Benchmark scripts and raw data&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I built an MCP Server that lets Claude manage your Substack</title>
      <dc:creator>André Ahlert</dc:creator>
      <pubDate>Sat, 14 Mar 2026 14:07:52 +0000</pubDate>
      <link>https://forem.com/andreahlert/i-built-an-mcp-server-that-lets-claude-manage-your-substack-1eb2</link>
      <guid>https://forem.com/andreahlert/i-built-an-mcp-server-that-lets-claude-manage-your-substack-1eb2</guid>
      <description>&lt;p&gt;The Substack web UI is fine for casual use, but if you're a power user who publishes daily, manages engagement, and wants to automate interactions, you need something faster.&lt;/p&gt;

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

&lt;p&gt;I built &lt;code&gt;@postcli/substack&lt;/code&gt;, a tool that gives you three interfaces to Substack:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. CLI&lt;/strong&gt; - Direct commands from your terminal&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;postcli-substack notes publish &lt;span class="s2"&gt;"Shipping fast from the terminal"&lt;/span&gt;
postcli-substack posts list &lt;span class="nt"&gt;--limit&lt;/span&gt; 5
postcli-substack feed &lt;span class="nt"&gt;--tab&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="nt"&gt;-you&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. TUI&lt;/strong&gt; - Full interactive terminal UI with 6 tabs&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;postcli-substack tui
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigate with j/k or arrow keys, scroll with mouse wheel, open posts in browser with 'o'. It's keyboard-driven and fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. MCP Server&lt;/strong&gt; - 16 tools for AI agents&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Tell Claude "like back everyone who liked my last note" and it just works.&lt;/p&gt;

&lt;h3&gt;
  
  
  The automation engine
&lt;/h3&gt;

&lt;p&gt;The part I'm most proud of is the automation engine. It uses SQLite to track processed entities (no duplicate actions) and supports triggers like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Someone likes your note → auto-like their latest note back&lt;/li&gt;
&lt;li&gt;New note from specific authors → auto-like or restack&lt;/li&gt;
&lt;li&gt;New post from specific publications → auto-restack&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Auth without API keys
&lt;/h3&gt;

&lt;p&gt;Substack doesn't have a public API. Auth works by extracting your existing Chrome session cookies (AES-128-CBC decryption) or manual cookie entry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;postcli-substack auth login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @postcli/substack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;89 tests. CI on Node 18/20/22. Open source (AGPL-3.0).&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/postcli/substack" rel="noopener noreferrer"&gt;https://github.com/postcli/substack&lt;/a&gt;&lt;br&gt;
NPM: &lt;a href="https://www.npmjs.com/package/@postcli/substack" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@postcli/substack&lt;/a&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>cli</category>
      <category>mcp</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
