<?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: Theo Brenner</title>
    <description>The latest articles on DEV Community by Theo Brenner (@theobrenner).</description>
    <link>https://dev.to/theobrenner</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4046016%2Faff3ecf9-4e15-4bde-befc-430a3a60ef1c.png</url>
      <title>DEV Community: Theo Brenner</title>
      <link>https://dev.to/theobrenner</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/theobrenner"/>
    <language>en</language>
    <item>
      <title>Designing a Comparison Schema When Features Aren't 1:1</title>
      <dc:creator>Theo Brenner</dc:creator>
      <pubDate>Sat, 25 Jul 2026 08:08:17 +0000</pubDate>
      <link>https://dev.to/theobrenner/designing-a-comparison-schema-when-features-arent-11-1pb2</link>
      <guid>https://dev.to/theobrenner/designing-a-comparison-schema-when-features-arent-11-1pb2</guid>
      <description>&lt;p&gt;I spent an embarrassing amount of time building comparison tables before admitting the data model was wrong. Not the UI — the schema underneath. Every "Tool A vs Tool B" page starts from an assumption that quietly poisons everything downstream: that both tools have the same features, and you just need to check boxes.&lt;/p&gt;

&lt;p&gt;They don't. That is usually the entire reason one exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The false equivalence problem is a schema problem
&lt;/h2&gt;

&lt;p&gt;The default schema is a matrix. Rows are features, columns are tools, cells are booleans.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- the version that ruins your dataset&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;comparison&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;feature_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tool_id&lt;/span&gt;    &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;supported&lt;/span&gt;  &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The failure is that &lt;code&gt;supported = false&lt;/code&gt; is doing at least five distinct jobs: the tool genuinely lacks this; the tool solves the same problem a completely different way so the feature name doesn't apply; it exists but only on a higher tier; it exists via a plugin rather than natively; or nobody checked.&lt;/p&gt;

&lt;p&gt;Rendering all five as a red ✗ is not a display bug you can fix in the template — the information was destroyed at write time. And the second case is where comparison sites do the most damage. Scoring a git-backed CLI tool as "✗ real-time collaborative editing" is technically true and completely misleading; the tool's answer to collaboration is branches and review, which is a different shape, not an absence.&lt;/p&gt;

&lt;p&gt;So the first schema change is: &lt;strong&gt;the cell is not a boolean.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;tool_capability&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;tool_id&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;capability_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;        &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- see enum below&lt;/span&gt;
  &lt;span class="n"&gt;tier_required&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;-- 'free' | 'pro' | 'enterprise' | NULL&lt;/span&gt;
  &lt;span class="n"&gt;via&lt;/span&gt;           &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;-- 'native' | 'plugin' | 'integration' | 'api'&lt;/span&gt;
  &lt;span class="n"&gt;note&lt;/span&gt;          &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;-- required when status = 'different_model'&lt;/span&gt;
  &lt;span class="n"&gt;evidence_url&lt;/span&gt;  &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;verified_at&lt;/span&gt;   &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capability_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status ∈ {
  full,              -- does the thing, no meaningful caveat
  partial,           -- does some of it; note explains the boundary
  different_model,   -- solves the problem, different concept; note REQUIRED
  not_applicable,    -- the concept does not exist in this tool's world
  absent,            -- genuine gap
  unknown            -- not verified; render as "—", never as ✗
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;different_model&lt;/code&gt; and &lt;code&gt;not_applicable&lt;/code&gt; are the two that make the dataset honest. &lt;code&gt;unknown&lt;/code&gt; is the one that keeps you honest — most comparison content is quietly full of unverified guesses rendered with the same confidence as tested facts. If your schema has no way to say "I don't know," your writers will say "no."&lt;/p&gt;

&lt;h2&gt;
  
  
  Capabilities are not features
&lt;/h2&gt;

&lt;p&gt;The second fix is renaming the row axis, which sounds cosmetic and isn't.&lt;/p&gt;

&lt;p&gt;Features are vendor vocabulary. One tool calls it Workspaces, another Projects, a third Teams, and a fourth has no such object because permissions attach directly to resources. If your rows are feature names, you have adopted one vendor's ontology as the neutral standard — usually the market leader's, which structurally flatters it and makes everyone else look incomplete.&lt;/p&gt;

&lt;p&gt;Model rows as &lt;strong&gt;user jobs&lt;/strong&gt; instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;capability&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;restrict&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;access&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;subset&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;per&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;group&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;people"&lt;/span&gt;
   &lt;span class="na"&gt;tool_a&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;full             (Workspaces + roles)&lt;/span&gt;
   &lt;span class="na"&gt;tool_b&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;different_model  (per-resource ACLs, no container object)&lt;/span&gt;
   &lt;span class="na"&gt;tool_c&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;partial          (all-or-nothing sharing, no groups)&lt;/span&gt;
   &lt;span class="na"&gt;tool_d&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;full             tier_required = 'enterprise'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;different_model&lt;/code&gt; is expressible without being a demerit, and the note carries the actual decision-relevant information: &lt;em&gt;tool_b can do this but you will configure it per resource, which is fine for 20 documents and miserable for 2,000.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Give capabilities their own metadata too, including an &lt;code&gt;objectivity&lt;/code&gt; field — &lt;code&gt;verifiable&lt;/code&gt; | &lt;code&gt;contextual&lt;/code&gt; | &lt;code&gt;taste&lt;/code&gt;. That column stops you from summing incomparable things. "Exports to CSV" is verifiable. "Feels fast" is taste. A single composite score that averages both is a number with no meaning, and it is the thing readers trust most.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing is a model, not a number
&lt;/h2&gt;

&lt;p&gt;Pricing deserves its own table because "$20/mo" is almost never comparable across two tools. The dimensions that actually differ:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unit of charge&lt;/strong&gt;: seat, workspace, usage, flat, hybrid&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free tier shape&lt;/strong&gt;: perpetual-limited vs. time-limited trial vs. none&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cliff&lt;/strong&gt;: which specific feature forces the upgrade (SSO, audit logs, API access — this is where the real cost hides)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaling behavior&lt;/strong&gt;: cost at 1, 5, 20, 100 seats is a curve, not a point&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So store &lt;code&gt;unit&lt;/code&gt;, &lt;code&gt;base_cents&lt;/code&gt;, &lt;code&gt;per_unit_cents&lt;/code&gt;, &lt;code&gt;included_units&lt;/code&gt;, and &lt;code&gt;captured_at&lt;/code&gt; — the model, not the headline number. Then you can render honest scenario costs ("at 12 seats") instead of a headline price that only applies to a solo user on annual billing. Every pricing row needs &lt;code&gt;captured_at&lt;/code&gt; because pricing rots faster than anything else in the dataset, and a stale price presented confidently is the fastest way to lose a reader permanently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provenance, or you are just writing opinions
&lt;/h2&gt;

&lt;p&gt;Two constraints I would now enforce at the database level rather than in review:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every claim carries evidence and a date.&lt;/strong&gt; &lt;code&gt;evidence_url&lt;/code&gt; and &lt;code&gt;verified_at&lt;/code&gt; are &lt;code&gt;NOT NULL&lt;/code&gt;. A claim older than your staleness threshold degrades to &lt;code&gt;unknown&lt;/code&gt; automatically rather than sitting there looking authoritative. This is the difference between a dataset and a blog post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comparisons are derived, never authored.&lt;/strong&gt; A "A vs B" page is a query over &lt;code&gt;tool_capability&lt;/code&gt;, not a hand-written row. Hand-written comparison rows drift out of sync with the tool pages within weeks and you will never notice. Making it derived means one write updates every page that references it, and it makes an entire class of contradiction impossible.&lt;/p&gt;

&lt;p&gt;Working through this on &lt;a href="https://stackalt.com" rel="noopener noreferrer"&gt;stackalt&lt;/a&gt; — where each entry has to say something real about pricing model, migration friction, and where a tool simply has no equivalent concept — the &lt;code&gt;different_model&lt;/code&gt; status ended up being the most-used non-&lt;code&gt;full&lt;/code&gt; value by a wide margin. Which is the finding, really: most tools that survive in the same category are not worse versions of each other. They disagree about what the problem is, and a boolean matrix has no vocabulary for disagreement.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Migration Friction Is the Real Cost of Switching Tools</title>
      <dc:creator>Theo Brenner</dc:creator>
      <pubDate>Fri, 24 Jul 2026 19:00:23 +0000</pubDate>
      <link>https://dev.to/theobrenner/migration-friction-is-the-real-cost-of-switching-tools-4cga</link>
      <guid>https://dev.to/theobrenner/migration-friction-is-the-real-cost-of-switching-tools-4cga</guid>
      <description>&lt;p&gt;Tool comparison posts obsess over feature matrices and monthly pricing. Both are the easy numbers. The expensive number is what it costs to &lt;em&gt;leave&lt;/em&gt;, and almost nobody publishes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three kinds of lock-in, in ascending order of pain
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Data lock-in&lt;/strong&gt; is the one people check. Can you export? In what format? A CSV dump that loses your relationship structure is not really an export.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow lock-in&lt;/strong&gt; is worse and less visible. Your team learned the tool's mental model. Your runbooks reference its UI. Your onboarding docs have screenshots. Switching means rewriting all of that, and none of it shows up in a pricing comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integration lock-in&lt;/strong&gt; is the killer. Every webhook, every CI step, every Zap pointing at this tool is a thing that breaks on migration day. The count grows silently — nobody tracks how many integrations a tool accumulates until they try to remove it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rough way to score it before you commit
&lt;/h2&gt;

&lt;p&gt;Before adopting anything, ask four questions and write the answers down:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Export fidelity&lt;/strong&gt; — can I get my data out in a form a competitor can actually ingest? Not "is there an export button."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration surface&lt;/strong&gt; — how many other systems will end up pointing at this? Each one is future migration work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config as code?&lt;/strong&gt; — if the configuration lives in a database behind a UI, migration means clicking. If it lives in YAML in my repo, migration means editing files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who owns the identity?&lt;/strong&gt; — if the tool is also your auth provider, leaving is a much bigger project than swapping a dependency.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Score each 1-5. A tool scoring badly on 3 and 4 needs to be &lt;em&gt;substantially&lt;/em&gt; better to justify adoption, not marginally better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the cheap option often is not
&lt;/h2&gt;

&lt;p&gt;The pattern I keep seeing: a team picks the cheaper tool, accumulates 20 integrations over 18 months, then discovers the migration cost exceeds three years of the price difference they were optimising for.&lt;/p&gt;

&lt;p&gt;Pricing is a recurring cost you can forecast. Migration friction is a one-time cost you cannot, and it lands at the worst possible moment — usually when the tool has already become a problem.&lt;/p&gt;

&lt;p&gt;I write up alternatives with this lens — pricing model, migration friction, and the trade-offs that actually bite — at &lt;a href="https://stackalt.com" rel="noopener noreferrer"&gt;StackAlt&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest caveat
&lt;/h2&gt;

&lt;p&gt;Sometimes lock-in is worth it. A deeply integrated tool that fits your workflow can be worth more than a portable one that does not. The argument is not "avoid lock-in" — it is "price it before you sign up," because the default is to not price it at all.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>saas</category>
      <category>architecture</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
