<?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>DEV Community: André Ahlert</title>
    <description>The latest articles on DEV Community by André Ahlert (@andreahlert).</description>
    <link>https://dev.to/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>DEV Community: André Ahlert</title>
      <link>https://dev.to/andreahlert</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/andreahlert"/>
    <language>en</language>
    <item>
      <title>Why I Built a Language Instead of a Framework</title>
      <dc:creator>André Ahlert</dc:creator>
      <pubDate>Sat, 16 May 2026 21:08:36 +0000</pubDate>
      <link>https://dev.to/andreahlert/why-i-built-a-language-instead-of-a-framework-5g65</link>
      <guid>https://dev.to/andreahlert/why-i-built-a-language-instead-of-a-framework-5g65</guid>
      <description>&lt;p&gt;A friend asked me, around the third week of working on &lt;a href="https://kilnx.org" rel="noopener noreferrer"&gt;Kilnx&lt;/a&gt;, why I had not just written it as an Express plugin. The question was fair. It was also the same question my own brain had been asking me for a month.&lt;/p&gt;

&lt;p&gt;The honest answer took me longer to find than I would like to admit. This piece is what I would have said to him in November if I had already understood what I was doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The push
&lt;/h2&gt;

&lt;p&gt;I had been shipping backends. Some of them were small. Most of them were not. A multi-tenant CRM where every query had to filter by &lt;code&gt;org_id&lt;/code&gt; or you leaked data across customers. An internal admin tool with role-based pages, scheduled jobs, outbound webhooks signed with HMAC. A SaaS dashboard with background workers, rate-limited APIs, an LLM call inside a critical path. None of these are blogs. All of them carried the same shape.&lt;/p&gt;

&lt;p&gt;The interesting work in each project was the domain. The uninteresting work was identical. Auth setup. Session management. CSRF wiring. Connection pool tuning. Migration script naming. Multi-tenant guards on every query. Webhook signature verification. The right way to call Claude from a background job without burning the bill on retry. By the time I had a first feature shipped, I had touched a dozen files and made forty decisions, none of which had anything to do with what the customer wanted.&lt;/p&gt;

&lt;p&gt;I started counting. Two thirds of the lines in those projects were about plumbing. The other third was the product. The numbers held across three projects. The numbers held across two stacks. The plumbing was not a project artifact. It was the toolchain's signature, and it was showing up in every project I touched.&lt;/p&gt;

&lt;p&gt;That is what pushed me toward a language. Not a feeling. A pattern that did not move when I changed teams, customers, or stacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Constitution
&lt;/h2&gt;

&lt;p&gt;The repo has a file called &lt;a href="https://github.com/kilnx-org/kilnx/blob/main/PRINCIPLES.md" rel="noopener noreferrer"&gt;PRINCIPLES.md&lt;/a&gt;. The first principle, numbered zero because it predates the others, reads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The complexity is the tool's fault, not the problem's. Most web apps are not complex. They are lists, forms, dashboards, CRUDs. The complexity comes from the tools we use, not from the problem we are solving. Kilnx exists to prove this.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence is a claim. The rest of the language is the test of the claim. If you accept the premise, the design follows. If you reject the premise, nothing about Kilnx makes sense. The interesting argument is whether the premise is true, not whether the design is clever.&lt;/p&gt;

&lt;p&gt;I think it is mostly true. Not entirely. Some web work is genuinely complex and would be complex in any tool. But the line between "complex problem" and "complex tool" runs further toward the tool side than most engineers want to admit, and the way to find out which side a given complexity sits on is to build a tool that subtracts itself and see what remains.&lt;/p&gt;

&lt;p&gt;Here is what a working slice of the language looks like. Authenticated task list, htmx delete, paginated query, all in one file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model task
  title: text required
  done: bool default false
  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/:id/delete requires auth
  query: DELETE FROM task WHERE id = :id AND owner = :current_user.id
  respond fragment delete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That file is the whole app. Registration, login with bcrypt, sessions, CSRF on the htmx POST, parameter binding on every query, pagination, ownership check on delete. The Express equivalent is between four hundred and six hundred lines across eight files, depending on which middleware you copy versus extract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The obvious objection
&lt;/h2&gt;

&lt;p&gt;Build a framework, not a language. Rails exists. Phoenix exists. Django exists. Whatever you think is broken about backend work, somebody already wrapped your favorite host language in a thinner thing and called it a framework. Pick one and contribute.&lt;/p&gt;

&lt;p&gt;That was the version of the argument I kept hearing, including from myself. It did not land for one structural reason. Frameworks always lose the constraint fight, and the constraint fight is exactly the fight Kilnx is trying to win.&lt;/p&gt;

&lt;p&gt;A framework lives inside the host language. The host language gives you escape hatches at every level. You want to bypass the router? Reach for the HTTP server. You want to skip the ORM? Drop into raw SQL. You want to ignore the tenant guard? Comment it out, the language will let you. Every framework I have shipped real work on, by month six, has half its codebase in the official patterns and the other half in escape hatches. The escape hatches are not bugs. They are the price the framework pays to live as a tenant inside a general-purpose language.&lt;/p&gt;

&lt;p&gt;I did not want a tenant. I wanted a contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a language can refuse
&lt;/h2&gt;

&lt;p&gt;Five things turned out to be impossible inside a framework that became natural inside a language.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compile-time SQL safety.&lt;/strong&gt; In any framework, your SQL lives in strings, or in an ORM that compiles strings, or in a query builder that pretends not to. The framework cannot validate your queries at compile time because the host language cannot see them at compile time. Kilnx queries are parsed by the same compiler that parses the rest of the program. A column rename in a model fails to compile every query that referenced it. SQL injection is not blocked by an escape function. It is blocked by the grammar refusing to interpolate untyped strings into SQL position.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-tenant guards as syntax.&lt;/strong&gt; Every SaaS backend I have shipped had the same bug class. Someone forgot to add &lt;code&gt;WHERE org_id = :current_user.org_id&lt;/code&gt; to a query, and one tenant could read another tenant's data. The bug class exists because the host language sees the missing filter as legal code. In Kilnx, a &lt;code&gt;tenant&lt;/code&gt; modifier on the model produces a fail-closed guard that the analyzer enforces on every query path. If you write a query that does not scope to the tenant, the compiler refuses to build the binary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model invoice tenant
  amount: int required
  customer: text required

page /invoices requires auth
  query invoices: SELECT id, amount, customer FROM invoice
  html
    {{each invoices}}&amp;lt;p&amp;gt;{customer}: {amount}&amp;lt;/p&amp;gt;{{end}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kilnx check app.kilnx
error: query in page /invoices is missing tenant scope
  app.kilnx:5: query invoices: SELECT id, amount, customer FROM invoice
  hint: tenant model 'invoice' requires WHERE org_id = :current_user.org_id
  hint: or use `unscoped: explicit-reason` to opt out for a specific query
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A framework can warn you. A language can refuse to build. The pattern is documented in the &lt;a href="https://github.com/kilnx-org/kilnx/pull/52" rel="noopener noreferrer"&gt;tenant rollout PR&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM agents as first-class language constructs.&lt;/strong&gt; The piece I wrote last week argued that agents in production end up as tasks on the orchestrator you already run. The same logic applies one layer down. An agent inside a request handler is a task on the language you already run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mcp linear
  command: linear-mcp-server
  env: LINEAR_API_KEY=:env.LINEAR_API_KEY

action /tickets/:id/triage
  agent classify
    prompt: "Classify ticket {ticket.body} into one of: bug, feature, support."
    permission-mode: plan
    max-budget-usd: 0.25
    max-turns: 3
    mcp: linear
  query: UPDATE ticket SET category = :classify.text, cost_usd = :classify.cost_usd
         WHERE id = :id
  respond fragment ticket-row
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;agent&lt;/code&gt; spawns a Claude CLI subprocess. &lt;code&gt;:classify.text&lt;/code&gt;, &lt;code&gt;:classify.session_id&lt;/code&gt;, &lt;code&gt;:classify.cost_usd&lt;/code&gt;, &lt;code&gt;:classify.stop_reason&lt;/code&gt; are bound for the rest of the action. &lt;code&gt;max-budget-usd&lt;/code&gt; is enforced by the runtime. &lt;code&gt;mcp: linear&lt;/code&gt; mounts the MCP server declared at the top of the file. The frame around the agent is grammar, not glue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migrations as a controlled surface.&lt;/strong&gt; &lt;code&gt;kilnx migrate&lt;/code&gt; detects drift across five dimensions: orphan columns, type mismatch, NOT NULL mismatch, single-column UNIQUE mismatch, DEFAULT presence mismatch. Migrations themselves are additive, never destructive. The language took a position on what is safe to do automatically and what requires the human to look.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kilnx migrate app.kilnx
applying schema...
warning: orphan column
  invoice.legacy_status (DB has it, model does not declare it)
  hint: drop manually after data migration
warning: type mismatch
  user.id (DB: integer, model: uuid)
  hint: requires data migration plus ALTER, not auto-generated
warning: NOT NULL mismatch
  task.due_date (DB: nullable, model: required)
  hint: backfill defaults before tightening
warning: UNIQUE mismatch
  account.slug (DB: not unique, model: unique)
  hint: dedupe rows before adding the constraint
warning: DEFAULT presence mismatch
  task.done (DB: no default, model: default false)
  hint: review before relying on the default in new code
migration applied with 5 warnings.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A framework can ship a migration tool. A language can make the migration tool part of the same compile pass that builds your routes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-binary deploy.&lt;/strong&gt; A framework runs on top of a runtime that you also have to deploy. Node. Python. Ruby. Each brings a package manager, a lockfile, a Dockerfile, a &lt;code&gt;node_modules&lt;/code&gt; directory the size of a small operating system. Kilnx compiles a &lt;code&gt;.kilnx&lt;/code&gt; file to a fifteen-megabyte binary that embeds the HTTP server, the database driver, the htmx JavaScript, and your application. &lt;code&gt;scp&lt;/code&gt; it to a server and run it. The deploy story is &lt;code&gt;./myapp&lt;/code&gt;. A framework can shrink the deploy story. A language can collapse it.&lt;/p&gt;

&lt;p&gt;Notice the pattern. A framework can make these things easier. A language can make their alternatives impossible. The asymmetry is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Building a language is more work than building a framework. This is the easy half of the trade-off to name. The repo is nineteen thousand lines of Go and three hundred eleven tests with race detection, to deliver something whose feature list on paper looks like a slightly opinionated web framework. If a small web framework was what I wanted, building the framework would have been the right answer.&lt;/p&gt;

&lt;p&gt;The harder half is that a language has to take itself seriously. The grammar has to be coherent. The error messages have to be useful. The tooling has to exist. There is no falling back on someone else's ecosystem when something is missing. You either ship the LSP server or your users do not get autocomplete. You either ship the test runner or your users do not get tests. You either ship the playground or your users cannot evaluate the language without installing it. You either auto-generate an &lt;a href="https://github.com/kilnx-org/kilnx/blob/main/AGENTS.md" rel="noopener noreferrer"&gt;AGENTS.md&lt;/a&gt; for coding agents or your users get LLMs inventing keywords that do not exist.&lt;/p&gt;

&lt;p&gt;The third cost is that a language refuses things, and refusing things is socially expensive. Every refusal is a fight with somebody who has a perfectly reasonable use case that the language does not serve. Frameworks can absorb those use cases with an escape hatch. Languages cannot. You have to look someone in the eye and tell them the language is not going to do that, and that the reason is that doing it would break the contract.&lt;/p&gt;

&lt;p&gt;A friend told me that the part of building a language nobody warns you about is that you have to say no to a lot of people who are right.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it gives back
&lt;/h2&gt;

&lt;p&gt;The give-back is the part that justifies the cost. It is also the part that does not fit on a marketing page, because it has to be measured rather than read about. So measure.&lt;/p&gt;

&lt;p&gt;The blog example in the repo is ninety-four lines in a single file. The Express equivalent is between four hundred and six hundred lines across eight files, depending on which middleware you copy versus extract.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Kilnx blog            Express + Prisma + EJS blog
──────────────────    ──────────────────────────────
app.kilnx        94   app.js                     62
                      routes/auth.js             88
                      routes/posts.js           104
                      models/Post.js             36
                      middleware/csrf.js         24
                      middleware/session.js      31
                      db/migrations/             47
                      views/                     94
                      ──────────                 ───
                      8 files                   486
1 file           94                            ~480
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Kilnx version is short because the language absorbed the rest, not because the app does less. The same things ship in both columns. The difference is which side wrote them.&lt;/p&gt;

&lt;p&gt;What disappears on the Kilnx side, never written by the user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bcrypt password hashing       auto from `auth`
session cookies, signed       auto from `auth`
CSRF on every POST/PUT/DELETE auto on every action
SQL parameter binding         only form the grammar allows
HTML escaping in templates    only form the grammar allows
multi-tenant scoping          refused at compile time if missing
schema migrations             same compile pass that builds routes
LLM agent budget enforcement  required attribute on `agent` blocks
MCP server lifecycle          managed by the runtime
HTTP server, routing, logs    embedded in the binary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other give-back is harder to measure but bigger. The constraints stop you from drifting. There is no point at which you can decide to do auth a different way and have it cost you nothing. The decision was made when the language was designed. You inherit it. The cognitive load of every project drops because the design space is smaller.&lt;/p&gt;

&lt;p&gt;For most product work, smaller design space is the gift you have been begging the universe for.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a framework is the right answer
&lt;/h2&gt;

&lt;p&gt;The inverse is real and worth naming.&lt;/p&gt;

&lt;p&gt;If your work needs escape hatches more often than it needs constraints, a framework is the right shape. Custom integrations against an irregular set of third-party systems, custom protocols, custom transport, custom auth flows that do not fit any standard pattern. Days that are ninety percent edge cases. A framework lets you write the edge case directly in the host language without the language fighting you.&lt;/p&gt;

&lt;p&gt;Convex made the same trade in the agent world. They accepted the determinism contract, and they paid the cost of not being able to do arbitrary side effects in mutations. For most product workloads that cost is fine. For some, it is too high. The same logic applies here. Kilnx accepts a constraint contract, and the contract is wrong for some workloads. The question is whether your workload is one of them, and the honest answer is usually no.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the bet really is
&lt;/h2&gt;

&lt;p&gt;The bet at the center of Kilnx is that a specific opinion, taken seriously, eats a category of work that nobody wanted to be doing anyway. Pick the right opinion, encode it past the point where users can opt out, and the opinion becomes leverage. The leverage shows up as code that did not need to be written. Two thirds of every project, in my experience.&lt;/p&gt;

&lt;p&gt;A framework can host an opinion. A language can enforce one. The reason I built a language is that I wanted the enforcement, and I had counted the lines of plumbing in enough projects to know what the enforcement was worth.&lt;/p&gt;

&lt;p&gt;Kilnx is in early release. The grammar is twenty-seven keywords. The compiler is a few thousand commits old. None of that matters as much as the bet does, and the bet is what is being tested in production over the next year.&lt;/p&gt;

&lt;p&gt;The spreadsheet was real. The language was the honest answer to it.&lt;/p&gt;




&lt;p&gt;I am building &lt;a href="https://kilnx.org" rel="noopener noreferrer"&gt;Kilnx&lt;/a&gt;, a declarative backend language that pairs with htmx, and &lt;a href="https://provero.org" rel="noopener noreferrer"&gt;Provero&lt;/a&gt;, where a lot of the language-shape decisions I write about are the day job. If the diagnosis here lands, that is the door.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;André Ahlert is a product engineer. Contributor across Apache, Flyte, Backstage, HTMX, Hyperscript. Currently building &lt;a href="https://kilnx.org" rel="noopener noreferrer"&gt;Kilnx&lt;/a&gt; and &lt;a href="https://provero.org" rel="noopener noreferrer"&gt;Provero&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>go</category>
      <category>htmx</category>
      <category>architecture</category>
    </item>
    <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://dev.to/andreahlert/soda-moved-to-elv2-provero-is-apache-20-42l9</link>
      <guid>https://dev.to/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://dev.to/andreahlert/i-built-a-backend-language-that-a-3b-model-writes-better-than-express-2im3</link>
      <guid>https://dev.to/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://dev.to/andreahlert/i-built-an-mcp-server-that-lets-claude-manage-your-substack-1eb2</link>
      <guid>https://dev.to/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>
