<?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: Alberto Arena</title>
    <description>The latest articles on DEV Community by Alberto Arena (@alberto_arena).</description>
    <link>https://dev.to/alberto_arena</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%2F1584618%2F8916c43c-3e5f-4896-b701-9e82ab63893f.jpeg</url>
      <title>DEV Community: Alberto Arena</title>
      <link>https://dev.to/alberto_arena</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alberto_arena"/>
    <language>en</language>
    <item>
      <title>The Laravel schema nobody chose</title>
      <dc:creator>Alberto Arena</dc:creator>
      <pubDate>Fri, 28 Aug 2026 11:16:10 +0000</pubDate>
      <link>https://dev.to/alberto_arena/the-laravel-schema-nobody-chose-510n</link>
      <guid>https://dev.to/alberto_arena/the-laravel-schema-nobody-chose-510n</guid>
      <description>&lt;p&gt;Fifteen real Laravel applications, every one of them a codebase you can go and read. 811 tables. One pinned MySQL 8.0 database, migrations only, no seed data. Every application installed from a clean clone and migrated before a single table was read.&lt;/p&gt;

&lt;p&gt;This is not a review of those fifteen projects. It is a census: a look at what a Laravel schema actually looks like once an application has shipped and grown past the tutorial stage. A handful of real issues turned up during the read, and each one is going to the project that owns it, privately, before anything about it is written here. What is left, once those are set aside, is more interesting anyway. These projects share conventions they never discussed, because the framework or the ecosystem around it chose first.&lt;/p&gt;

&lt;p&gt;Two different reads sit behind this piece, and they cover different sets, so I keep them apart throughout. The relationship counts cover all fifteen applications. The structural checks are slower, and some of these applications could not run the tooling at the time, so they cover a smaller set: twelve databases and 482 tables, of which seven are real applications and five are bare Laravel baselines kept as a control. Wherever a number appears below, it says which of the two it came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  A table nobody wrote on purpose
&lt;/h2&gt;

&lt;p&gt;Six tables across four of these projects have no primary key at all. In Bagisto there are three of them, &lt;code&gt;password_resets&lt;/code&gt;, &lt;code&gt;admin_password_resets&lt;/code&gt; and &lt;code&gt;customer_password_resets&lt;/code&gt;. Firefly III and Koel have one each under the first of those names. Twill has one under its own prefix. All six carry the same shape:&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="n"&gt;email&lt;/span&gt;        &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&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;token&lt;/span&gt;        &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&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;created_at&lt;/span&gt;   &lt;span class="nb"&gt;timestamp&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;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;-- and nothing declared PRIMARY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That shape is not a mistake any of these four projects made. It is Laravel's own old scaffold, the one every project generated for years by default. Laravel has since replaced it: a current Laravel 13 install ships a table named &lt;code&gt;password_reset_tokens&lt;/code&gt;, with the email column as an actual primary key.&lt;/p&gt;

&lt;p&gt;Twill is the clearest evidence of what happened here. At some point it renamed its copy to &lt;code&gt;twill_password_resets&lt;/code&gt;, its own name in its own migration, and kept the old keyless design anyway. Nobody sat down and decided a password reset table should have no primary key. A framework generator made that call years ago, and it has been carried forward ever since by projects that had no reason to look at it again.&lt;/p&gt;

&lt;p&gt;Read this one carefully: it says something about how Laravel scaffolding propagates, not about carelessness in any of the four projects that still carry it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How connected is a real schema, actually
&lt;/h2&gt;

&lt;p&gt;The strongest finding in this census did not come from the structural rules at all. It came from a much simpler question, asked of all fifteen applications: of all the tables in a schema, how many actually sit in at least one foreign key relationship with another table?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App&lt;/th&gt;
&lt;th&gt;Tables&lt;/th&gt;
&lt;th&gt;Foreign keys&lt;/th&gt;
&lt;th&gt;Tables in a relationship&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/bagisto/bagisto" rel="noopener noreferrer"&gt;Bagisto&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;146&lt;/td&gt;
&lt;td&gt;185&lt;/td&gt;
&lt;td&gt;87%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/monicahq/monica" rel="noopener noreferrer"&gt;Monica&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;138&lt;/td&gt;
&lt;td&gt;86%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/lunarphp/lunar" rel="noopener noreferrer"&gt;Lunar&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;82&lt;/td&gt;
&lt;td&gt;85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/firefly-iii/firefly-iii" rel="noopener noreferrer"&gt;Firefly III&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;81&lt;/td&gt;
&lt;td&gt;114&lt;/td&gt;
&lt;td&gt;79%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/koel/koel" rel="noopener noreferrer"&gt;Koel&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/pterodactyl/panel" rel="noopener noreferrer"&gt;Pterodactyl&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;74%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/InvoiceShelf/InvoiceShelf" rel="noopener noreferrer"&gt;InvoiceShelf&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;79&lt;/td&gt;
&lt;td&gt;69%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/LycheeOrg/Lychee" rel="noopener noreferrer"&gt;Lychee&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;67%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/Azuriom/Azuriom" rel="noopener noreferrer"&gt;Azuriom&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;64%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/area17/twill" rel="noopener noreferrer"&gt;Twill&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;33%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/cachethq/cachet" rel="noopener noreferrer"&gt;Cachet&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;16%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/BookStackApp/BookStack" rel="noopener noreferrer"&gt;BookStack&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/snipe/snipe-it" rel="noopener noreferrer"&gt;Snipe-IT&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/octobercms/october" rel="noopener noreferrer"&gt;October CMS&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/statamic/cms" rel="noopener noreferrer"&gt;Statamic&lt;/a&gt; is the fifteenth and is not in this table, for a reason that gets its own section below.&lt;/p&gt;

&lt;p&gt;Look at the bottom of that list. Snipe-IT is a mature asset manager with 58 tables held together by exactly one foreign key. October CMS has 41 tables and none at all: its core modules carry 35 relation declarations in plain PHP, &lt;code&gt;belongsTo&lt;/code&gt;, &lt;code&gt;hasMany&lt;/code&gt;, &lt;code&gt;morphTo&lt;/code&gt;, the usual Eloquent vocabulary, and not one line outside &lt;code&gt;vendor/&lt;/code&gt; ever calls &lt;code&gt;-&amp;gt;foreign()&lt;/code&gt;. The relationships are real. The database enforces none of them.&lt;/p&gt;

&lt;p&gt;Put another way: half of this corpus would draw an ER diagram (ERD) with almost no lines in it, not because the relationships are missing, but because they live somewhere a diagram cannot see.&lt;/p&gt;

&lt;p&gt;That is not a defect anywhere on this list. It is a real fork in how Laravel applications get built, sitting in plain sight, that nobody had put a number on before. One half of this corpus leans on the database to hold its relationships together. The other half keeps every one of them in application code and asks nothing of the schema. Both are working software. They are just built on two different assumptions about what a database is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Portability has a cost, and it is the foreign key
&lt;/h2&gt;

&lt;p&gt;Part of that split has a plain explanation. Several of these projects ship migrations designed to run on more than one database engine, MySQL, PostgreSQL and SQLite alike. That choice has a direct, visible cost in the schema, and the foreign key is the main casualty.&lt;/p&gt;

&lt;p&gt;Cachet is the clearest example: 32 tables, and 3 foreign keys between them. Its schema is full of columns that are clearly meant to reference another table, with &lt;code&gt;_id&lt;/code&gt; naming, the right type, sitting right next to the table they point at, and no constraint declared. A &lt;code&gt;-&amp;gt;foreign()&lt;/code&gt; call is tied to a specific engine's constraint syntax in a way a plain integer column is not, so a schema meant to run anywhere gives up the constraint to keep that promise.&lt;/p&gt;

&lt;p&gt;This is worth holding on to before reading the bottom of the table as a quality signal. A self-hostable project that has to work on whatever engine somebody happens to have is under a constraint that a private application, built for one engine from day one, never faces.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a starter kit adds, and what a flat-file CMS does not
&lt;/h2&gt;

&lt;p&gt;Two results here are not surprises, and both are worth stating plainly, because they rule things out.&lt;/p&gt;

&lt;p&gt;The first is about starter kits. The structural read included a bare Laravel skeleton as a control, migrated with nothing else installed, and three of the common ways a project gets started on top of it. Breeze and Filament both landed on exactly 9 tables, the same count as the bare control, with nothing flagged at either. Jetstream adds five more tables for team support and still comes back with nothing flagged under the default profile. So the schema a brand new Laravel project starts with does not really depend on which of those gets picked. That is worth knowing before assuming a schema problem in a young project came from a starter kit choice, because it almost certainly did not.&lt;/p&gt;

&lt;p&gt;The second is Statamic, and it is why it sits outside the table above. Statamic scored exactly the same as the bare control: 9 tables, nothing flagged. That is the expected result rather than a surprising one, and it is easy to forget while reading a column of percentages. Statamic is a flat-file CMS. Content lives in files, not rows. Its database only exists for the parts of Laravel that need one, sessions, cache, the usual scaffolding. Ranking it on how connected its schema is would say nothing, because it has no application tables to connect.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does and does not show
&lt;/h2&gt;

&lt;p&gt;Every codebase here publishes its source, but the licence is not what creates the bias, and it is worth being precise about that. Twelve of these fifteen are MIT, AGPL or GPL. Three are commercial products that happen to ship readable source: Cachet, October CMS and Statamic. What all fifteen actually share is a distribution model. They ship to infrastructure they do not control, so their migrations have to run on whatever engine the person installing them happens to have.&lt;/p&gt;

&lt;p&gt;That is the pressure that suppresses foreign keys, and it falls just as hard on the three commercial ones as on the twelve. A private application, built for one engine from day one and deployed only by the team that wrote it, is under none of it and will look far more connected as a result. That is a different population, and this census cannot see it.&lt;/p&gt;

&lt;p&gt;One application that was meant to be in this set never finished. Coolify's migrations stop partway on MySQL: one index name resolves to 72 characters, past MySQL's limit of 64. That is not a defect in Coolify. It is a PostgreSQL-first application being exactly that, on an engine it was never designed for, and it could not be rescued by switching engines, because one pinned MySQL is the first rule of this setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The instrument, briefly
&lt;/h2&gt;

&lt;p&gt;Every schema here was read with &lt;a href="https://trussphp.com/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=schema-census" rel="noopener noreferrer"&gt;&lt;code&gt;truss:doctor&lt;/code&gt;&lt;/a&gt;, a structural check I built into Laravel Truss. It is the reason this census could happen at all: it is what turned hundreds of tables across fifteen databases into something one person could actually read in a reasonable amount of time. Structure only, never data.&lt;/p&gt;

&lt;p&gt;It is not the subject of this piece, and any real issue it turned up along the way goes to the project that owns it before it goes anywhere public. That is not a promise made only here: the &lt;a href="https://trussphp.com/reference/tested-applications/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=schema-census" rel="noopener noreferrer"&gt;list of applications it has been run against&lt;/a&gt; says the same thing, and deliberately publishes no findings.&lt;/p&gt;

&lt;p&gt;What is public is the part that is not anybody's fault. A lot of what looks like a schema decision in a mature Laravel application was never really decided by that application at all.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>database</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The bug that only showed up once strangers could paste a schema</title>
      <dc:creator>Alberto Arena</dc:creator>
      <pubDate>Mon, 24 Aug 2026 12:14:22 +0000</pubDate>
      <link>https://dev.to/alberto_arena/the-bug-that-only-showed-up-once-strangers-could-paste-a-schema-92j</link>
      <guid>https://dev.to/alberto_arena/the-bug-that-only-showed-up-once-strangers-could-paste-a-schema-92j</guid>
      <description>&lt;p&gt;People kept asking the same question about the schema viewer I maintain: "what does this look like on &lt;em&gt;my&lt;/em&gt; schema?" The demo only ever offered a sample database to look at, so the honest answer was "install the package and point it at yours." That's a fair amount of friction for someone who just wants to see whether the tool is worth their time.&lt;/p&gt;

&lt;p&gt;So I added a page that parses a pasted &lt;code&gt;mysqldump&lt;/code&gt; in the browser and draws it with the same dashboard the package ships. No install, no upload, nothing leaves the tab. That part was the easy sell. The part worth writing about is everything the code had been getting away with, because until that page existed, every schema it had ever drawn came from a database connection the person running it had configured themselves.&lt;/p&gt;

&lt;p&gt;A connection string is a small trust decision that's easy to forget you made. You typed it, or an environment variable did, and either way something you controlled put it there. A pasted file has no such history. It could be a colleague's export, a tutorial's sample data, or nine hundred lines of MySQL DDL from someone you've never met. Same parser, same renderer, same code path, wildly different set of things it's now allowed to assume.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assumption one: names are just strings
&lt;/h2&gt;

&lt;p&gt;The dashboard builds a diagram definition by writing table and column names straight into it. That's fine when a name came from &lt;code&gt;information_schema&lt;/code&gt;, because a database enforces its own identifier rules before a name can exist. It stops being fine the moment a name can come from anywhere: nothing stops a pasted file from naming a column something that isn't a database identifier at all, it's a fragment of markup for whatever's rendering the diagram.&lt;/p&gt;

&lt;p&gt;Read from a live connection, that string had already been validated by something else before it ever reached this code. Read from a paste, the validation had never happened, and the rendering code had no way to know the difference because it never checked. The fix wasn't clever: every identifier gets reduced to what the diagram format actually accepts before it's written anywhere, consistently, on every reference to that name. But it's worth sitting with what made the bug possible in the first place: correct code, wrong assumption. The string-writing was never wrong. What was wrong was believing every string that reached it had already been through a gate it hadn't actually been through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assumption two: an unhandled case can stay quiet
&lt;/h2&gt;

&lt;p&gt;A schema dump is not one grammar, it's whatever the tool that produced it decided to write, and that varies more than you'd expect: backtick-quoted identifiers or not, a &lt;code&gt;CREATE INDEX&lt;/code&gt; as its own statement or folded into the table definition, foreign keys added later with &lt;code&gt;ALTER TABLE&lt;/code&gt; instead of declared inline. A parser that only handles the common shapes will hit a line it doesn't recognize eventually, and the question is what happens next.&lt;/p&gt;

&lt;p&gt;The tempting answer is: skip it and move on, the diagram still mostly works. That's the wrong answer for a page whose whole job is showing someone their actual structure. A single dropped constraint doesn't make the diagram look broken. It makes it look &lt;em&gt;complete and wrong&lt;/em&gt;, which is worse, because nothing about the output tells the reader to doubt it. So every statement now gets one of three outcomes, and all three are visible: parsed and drawn, deliberately skipped and counted, or flagged with the line number that produced it. A summary line reports the second and third categories up front, not buried in a details panel nobody opens. If ten tables came from a real database and the diagram shows nine, silence is the bug. Saying so is the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assumption three: an unnamed relationship isn't a real one
&lt;/h2&gt;

&lt;p&gt;Truss has an inference mode elsewhere that guesses relationships from naming conventions, &lt;code&gt;user_id&lt;/code&gt; probably points at &lt;code&gt;users&lt;/code&gt; even without a declared foreign key. That's a genuinely useful feature when you opted into it and can eyeball the result against a schema you know. It's a bad default for a page whose output is the first and only thing a stranger sees. A guessed edge that's wrong doesn't read as a guess. It reads as the tool telling you something false about your own database, with no indication it was ever uncertain.&lt;/p&gt;

&lt;p&gt;So the paste-parser draws only what's explicitly declared, a &lt;code&gt;FOREIGN KEY&lt;/code&gt; constraint or nothing. A column named exactly like a foreign key convention, with no constraint behind it, produces no edge at all. That's a narrower feature than the inference mode elsewhere in the same package, on purpose: precision matters more than completeness when the reader has no way to independently check your work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that hid behind a passing test
&lt;/h2&gt;

&lt;p&gt;One more, because it's not about untrusted input at all and I'd have missed it without a different kind of check. Reviewing the page before it shipped, every click led back to the project's GitHub repo, no matter what part of the page you clicked. The cause was a missing closing &lt;code&gt;&amp;lt;/a&amp;gt;&lt;/code&gt; tag, several lines up, that had nested the entire rest of the shell inside a single link.&lt;/p&gt;

&lt;p&gt;The existing tests for that page matched strings with regular expressions. A regex has no concept of "this tag is still open ten lines later." It saw the opening &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;, saw a plausible chunk of markup after it, and passed, the same way it would have passed if that tag had been closed correctly three lines down instead of never. What caught the bug in the end was a structural check, one that actually parses the HTML and confirms every tag that opens also closes, and the self-test that check needed was the same broken markup fed back into it on purpose.&lt;/p&gt;

&lt;p&gt;The lesson generalizes past HTML: a test that pattern-matches text is checking that certain substrings exist, not that the thing you shipped has the shape you think it has. Those aren't the same guarantee, and the gap between them is exactly where a test suite stays green while a page silently breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structure only stays true, on purpose
&lt;/h2&gt;

&lt;p&gt;None of this changed what the tool promises: structure only, never data. Any &lt;code&gt;INSERT&lt;/code&gt; statement in a pasted dump gets counted and discarded, not read, and that count is reported so the promise is something you can watch rather than something you have to take on faith. The parsing itself runs entirely in page code, the result reaches the dashboard through an in-memory blob URL, and the only thing written to &lt;code&gt;sessionStorage&lt;/code&gt; is used to survive a page reload and gets deleted the moment it's read. Nothing is uploaded, because there's nowhere on a server for it to go.&lt;/p&gt;

&lt;p&gt;That promise was never the hard part. The hard part was noticing how many other things the code had quietly been trusting, once "your own database connection" stopped being the only way data could arrive.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://github.com/albertoarena/laravel-truss" rel="noopener noreferrer"&gt;Truss&lt;/a&gt; is the Laravel schema viewer I maintain. You can try it on your own schema, no install required, at &lt;a href="https://trussphp.com/demo/your-schema/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=the-bug-that-only-showed-up-with-pasted-schemas" rel="noopener noreferrer"&gt;trussphp.com/demo/your-schema&lt;/a&gt;. Structure only, never data.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>database</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Context Engineering: The Discipline That Keeps AI From Writing Slop</title>
      <dc:creator>Alberto Arena</dc:creator>
      <pubDate>Mon, 17 Aug 2026 10:43:55 +0000</pubDate>
      <link>https://dev.to/alberto_arena/context-engineering-the-discipline-that-keeps-ai-from-writing-slop-4ab0</link>
      <guid>https://dev.to/alberto_arena/context-engineering-the-discipline-that-keeps-ai-from-writing-slop-4ab0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;"The art of providing all the context for the task to be plausibly solvable by the LLM."&lt;/p&gt;

&lt;p&gt;Tobi Lütke, Shopify CEO, on &lt;a href="https://x.com/tobi/status/1935533422589399127" rel="noopener noreferrer"&gt;X&lt;/a&gt;, June 19, 2025&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the definition Tobi Lütke gave the term "context engineering" when he decided he liked it better than "prompt engineering." I think it's the right definition, and I think most people complaining about AI slop are one inference away from noticing why.&lt;/p&gt;

&lt;p&gt;A vibecoder who fires a prompt at an agent with no project context isn't getting a worse model than the one I use. They're getting the same model doing its honest best with nothing to ground it: no rules file, no memory of yesterday's decisions, no pointer to the component that already solves this. So it invents something plausible. Inconsistent naming, a reinvented helper that already exists three files over, an architecture assumption that was wrong two refactors ago. That's not the model being bad. That's the model being under-informed, confidently.&lt;/p&gt;

&lt;p&gt;Anthropic, LangChain, and the team behind Manus have all converged on the same finding from production agents: most agent failures are context failures, not model failures. Bad context in, sloppy software out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four ways context goes wrong
&lt;/h2&gt;

&lt;p&gt;The clearest breakdown of &lt;em&gt;how&lt;/em&gt; context fails comes from Drew Breunig's &lt;a href="https://www.dbreunig.com/2025/06/22/how-contexts-fail-and-how-to-fix-them.html" rel="noopener noreferrer"&gt;How Long Contexts Fail&lt;/a&gt;: poisoning, distraction, confusion, and clash. What follows is my own relabeling of those four, translated into what each one actually looks like in a codebase rather than restated in Breunig's original terms:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure mode&lt;/th&gt;
&lt;th&gt;What it looks like&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Burst&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Everything gets dumped into context upfront, so the one relevant fact is buried under forty irrelevant ones and the agent weighs them about equally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Poisoning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One wrong or hallucinated detail early in a session (a method that doesn't exist, a convention that was never real) gets treated as fact for the rest of it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Noise&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The context is technically all relevant, just too much of it, so the signal-to-text ratio collapses and the agent can't tell what actually matters right now&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Conflict&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The system prompt says one thing, the project's rules file says another, and the agent has to guess which one wins, silently, every time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these are model problems. They're all things a human decided, or failed to decide, about what the agent sees and when. Which means they're fixable the same way any other engineering problem is fixable: on purpose, not by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like when it's done on purpose
&lt;/h2&gt;

&lt;p&gt;I'd rather show this than diagram it, so here's the actual &lt;code&gt;CLAUDE.md&lt;/code&gt; from &lt;a href="https://github.com/albertoarena/laravel-truss" rel="noopener noreferrer"&gt;Truss&lt;/a&gt;, a Laravel package I maintain. It's &lt;a href="https://github.com/albertoarena/laravel-truss/blob/main/CLAUDE.md" rel="noopener noreferrer"&gt;52 lines long&lt;/a&gt;, it states the stack, the commands, and a short list of invariants that must never break, like "no data exposed, ever, only structure," and then it stops:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Pointers&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Architecture and domain model: &lt;span class="sb"&gt;`docs/DESIGN.md`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Phased build plan: &lt;span class="sb"&gt;`docs/INSTRUCTIONS.md`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Decision log: &lt;span class="sb"&gt;`docs/DECISIONS.md`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Path-scoped rules (auto-load when matching files are touched): &lt;span class="sb"&gt;`.claude/rules/`&lt;/span&gt;

This file should stay short enough to read in under a minute. If you're
about to add detail, it probably belongs in &lt;span class="sb"&gt;`docs/`&lt;/span&gt; instead, with a
pointer added here.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything that isn't needed on every single turn lives one hop away instead of sitting resident in context by default. That's not a documentation choice, it's a context budget: the file names its own constraint and holds itself to it. Those &lt;code&gt;.claude/rules/&lt;/code&gt; files are real, not a placeholder: &lt;code&gt;introspection.md&lt;/code&gt; scopes to &lt;code&gt;src/Introspection/**&lt;/code&gt;, &lt;code&gt;frontend.md&lt;/code&gt; to &lt;code&gt;resources/js&lt;/code&gt;, &lt;code&gt;resources/css&lt;/code&gt;, and &lt;code&gt;resources/views&lt;/code&gt;, &lt;code&gt;release.md&lt;/code&gt; to &lt;code&gt;CHANGELOG.md&lt;/code&gt;, each one loading only when Claude actually touches a matching path, not resident by default the way &lt;code&gt;CLAUDE.md&lt;/code&gt; itself is. I've written up the fuller mechanics of how that split works, RAM versus demand-paged rules versus skills versus disk, in &lt;a href="https://dev.to/posts/claude-md-skills-are-not-disk/"&gt;CLAUDE.md Is RAM, Skills Are Not Disk&lt;/a&gt;, if you want the reasoning behind the shape rather than just the result.&lt;/p&gt;

&lt;p&gt;The point here isn't "copy this file." It's that the file is small enough to &lt;em&gt;audit&lt;/em&gt;. Compare that to the ordinary case: a three-hundred-line &lt;code&gt;CLAUDE.md&lt;/code&gt; nobody's re-read in months isn't context engineering, it's context archaeology, and the agent is the one doing the digging, badly, every session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three checks, not a checklist
&lt;/h2&gt;

&lt;p&gt;A checklist gets skimmed once and forgotten. What actually holds a team to this is something mechanical enough to enforce, so here are three checks, in ascending effort, none of which require trusting anyone's discipline going forward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A line budget.&lt;/strong&gt; Truss's &lt;code&gt;CLAUDE.md&lt;/code&gt; states its own limit in prose, "short enough to read in under a minute", but doesn't enforce it, and reading time isn't something CI can check directly. Line count is the closest cheap proxy, so here's what that could look like as a CI step (120 is my own suggested threshold, not a number Truss enforces today):&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Keep CLAUDE.md readable in under a minute&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;LINES=$(wc -l &amp;lt; CLAUDE.md)&lt;/span&gt;
    &lt;span class="s"&gt;if [ "$LINES" -gt 120 ]; then&lt;/span&gt;
      &lt;span class="s"&gt;echo "CLAUDE.md is $LINES lines, over the 120-line budget. Move detail to docs/."&lt;/span&gt;
      &lt;span class="s"&gt;exit 1&lt;/span&gt;
    &lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cheap, mechanical, and it's enforcing a rule the file already claims for itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A drift check.&lt;/strong&gt; This is the one I trust most, because it's the same idea Truss already applies to database schemas. A committed export file, checked against the live schema, build fails if they've drifted apart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan truss:export &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;schema.dbml &lt;span class="nt"&gt;--check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It compares the freshly generated export against whatever's already committed at that path and exits non-zero if they don't match byte for byte, the same mechanism as &lt;code&gt;git diff --exit-code&lt;/code&gt; for a generated file, just schema-aware. Context files rot the exact same way, except what's usually committed is nothing: no file that gets checked against the thing it describes. Does every path &lt;code&gt;CLAUDE.md&lt;/code&gt; points to still exist? Was &lt;code&gt;docs/DESIGN.md&lt;/code&gt; last touched &lt;em&gt;before&lt;/em&gt; the architecture it describes changed? A pre-commit hook or CI step that resolves every referenced path and flags anything missing or stale catches the failure mode that actually bites: an agent working confidently off documentation that quietly stopped being true.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Golden-task regression checks.&lt;/strong&gt; A small, fixed set of representative prompts, "add a new artisan command," "explain the authorization gate", run periodically against whatever the current context bundle produces, with the expected shape of the answer written down somewhere. This one doesn't fully automate. But even a manual pass before a release catches the case the other two checks can't: a change to a rules file that's individually reasonable but breaks something two files away, silently, because nobody asked the agent to actually do the task before shipping the instruction change.&lt;/p&gt;

&lt;p&gt;None of these are exotic. They're linting, drift detection, and regression testing, the same three things you'd already reach for if the artifact in question were code instead of prose. That's the actual point: it is code, in every way that matters except syntax highlighting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The discipline is the whole difference
&lt;/h2&gt;

&lt;p&gt;The vibecoder fires prompts into a context vacuum and ships whatever comes back, because "prompting" is the only lever they know is there. The engineer treats context, the rules file, the memory, the docs it points to, as a first-class artifact: versioned, reviewed, budgeted, checked for drift, same as anything else that ships. Same model, same tool, wildly different output, and the difference was never the prompt.&lt;/p&gt;

&lt;p&gt;If you're looking at a vibe-coded app that feels inconsistent, half-finished, confidently wrong about how your own codebase works, don't ask what model built it. Ask what it was allowed to see.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>devops</category>
    </item>
    <item>
      <title>My coding agent kept inventing columns</title>
      <dc:creator>Alberto Arena</dc:creator>
      <pubDate>Mon, 10 Aug 2026 14:30:52 +0000</pubDate>
      <link>https://dev.to/alberto_arena/my-coding-agent-kept-inventing-columns-1a6c</link>
      <guid>https://dev.to/alberto_arena/my-coding-agent-kept-inventing-columns-1a6c</guid>
      <description>&lt;p&gt;Ask a coding agent to write a query against a table it hasn't seen this session, and it will guess. Confidently, plausibly, and wrong: a foreign key named &lt;code&gt;author_id&lt;/code&gt; when the column is actually &lt;code&gt;created_by&lt;/code&gt;, a &lt;code&gt;status&lt;/code&gt; treated as a free-text string when it's a &lt;code&gt;tinyint&lt;/code&gt; enum, a table it's sure exists because a table like it usually does. It isn't lying, it just doesn't know, and nothing forces it to say so.&lt;/p&gt;

&lt;p&gt;The usual fix is pasting a schema dump into the chat at the start of a session. That works until the next migration, at which point it's just a different kind of wrong: confidently out of date instead of confidently invented.&lt;/p&gt;

&lt;p&gt;There's an obvious third option: let the agent run queries itself, most coding agents can already execute SQL. But that means handing over real database credentials, and a connection that can query can also see rows, not just structure. That's a bigger grant than the problem needs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/albertoarena/laravel-truss" rel="noopener noreferrer"&gt;Truss&lt;/a&gt; has always been a live, zoomable ER diagram of your Laravel app's real database schema, structure only, never a row of data. Version 1.8 points that same live structure at a coding agent instead of a browser tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give it meaning a type can't carry
&lt;/h2&gt;

&lt;p&gt;A column tells an agent its name and type, not what it means. &lt;code&gt;status = 1&lt;/code&gt; doesn't say "paid" on its own. Annotate it once and every export carries it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/truss.php&lt;/span&gt;
&lt;span class="s1"&gt;'annotations'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'source'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'config'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'database'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'tables'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'orders'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'note'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'One row per checkout attempt, not per completed order.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'columns'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'tinyint: 0 pending, 1 paid, 2 refunded'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;],&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;If your database already carries &lt;code&gt;COMMENT&lt;/code&gt; strings on tables and columns, leave &lt;code&gt;'database'&lt;/code&gt; in &lt;code&gt;annotations.source&lt;/code&gt; and Truss reads those directly, no duplicate config to keep in sync. Either way, a comment is part of the &lt;code&gt;CREATE TABLE&lt;/code&gt; definition, not a row: still structure only. Strip them from any single export with &lt;code&gt;--no-annotations&lt;/code&gt; when you just want the bare shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trim it to what the question needs
&lt;/h2&gt;

&lt;p&gt;A forty-table schema is a lot of tokens to spend on a question about one table. &lt;code&gt;--compact&lt;/code&gt; drops column defaults and non-unique indexes without dropping a single table, column, or foreign key. &lt;code&gt;--focus=orders --depth=1&lt;/code&gt; narrows the export to one table and its foreign-key neighbourhood, the same idea as the dashboard's focus mode, now available from the command line. And there's a new &lt;code&gt;llm&lt;/code&gt; format alongside the existing five (DBML, JSON, CSV, Markdown, Mermaid), a dense plaintext export tuned for a token budget rather than for a human reading a data dictionary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan truss:export &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;llm &lt;span class="nt"&gt;--focus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;orders &lt;span class="nt"&gt;--depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;--compact&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's what the export produces. Calling it is just as direct, in code or over HTTP.&lt;/p&gt;

&lt;p&gt;Building the same thing in code goes through a new fluent, immutable &lt;code&gt;Truss&lt;/code&gt; facade instead of the command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Truss&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;compact&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toDbml&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a gated &lt;code&gt;GET {prefix}/export/{format}&lt;/code&gt; route serves the identical output to any HTTP client, behind the same &lt;code&gt;viewTruss&lt;/code&gt; gate as the dashboard. Command, facade, route, dashboard download: one pipeline underneath all four, so they can never quietly disagree with each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask it live, instead of pasting a snapshot
&lt;/h2&gt;

&lt;p&gt;The part I actually wanted, though, wasn't a better export. It was not exporting anything at all.&lt;/p&gt;

&lt;p&gt;Truss 1.8 adds an optional server for MCP, the Model Context Protocol that Claude Code, Claude Desktop, and Cursor use to reach outside tools. Built on &lt;code&gt;laravel/mcp&lt;/code&gt;, it talks to a coding agent directly over local stdio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require laravel/mcp
php artisan mcp:start truss
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point one at it and the agent gets five tools, &lt;code&gt;list_tables&lt;/code&gt;, &lt;code&gt;describe_table&lt;/code&gt;, &lt;code&gt;get_schema&lt;/code&gt;, &lt;code&gt;focus_table&lt;/code&gt;, and &lt;code&gt;get_structural_review&lt;/code&gt;, plus a &lt;code&gt;truss://schema&lt;/code&gt; resource, all reading the live schema on demand. Every tool advertises MCP's &lt;code&gt;readOnlyHint&lt;/code&gt;, so a client can present them as read-only instead of prompting for write approval on a call that was never going to write anything. No row data, ever, and the same exclusion and managed-connection safeguards as the rest of Truss apply here too, opt-in and off by default behind &lt;code&gt;truss.mcp.enabled&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I pointed it at a real project I've worked on for a while, in Claude Desktop, and the difference was immediate: instead of me pasting a schema dump at the start of the conversation, or the agent asking me to run a query to check a column name, it just called &lt;code&gt;describe_table&lt;/code&gt; before it wrote anything, the same check that would have caught the guessed &lt;code&gt;author_id&lt;/code&gt; from the start of this post. No staleness, because there's nothing to go stale, it's reading the same live introspection the diagram uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://trussphp.com/demo/" rel="noopener noreferrer"&gt;Live demo&lt;/a&gt;, running against a fictional schema&lt;/li&gt;
&lt;li&gt;AI context guide: &lt;a href="https://trussphp.com/guides/ai-context/" rel="noopener noreferrer"&gt;trussphp.com/guides/ai-context&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MCP server guide: &lt;a href="https://trussphp.com/guides/mcp-server/" rel="noopener noreferrer"&gt;trussphp.com/guides/mcp-server&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Full changelog: &lt;a href="https://github.com/albertoarena/laravel-truss/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Update with &lt;code&gt;composer update albertoarena/laravel-truss&lt;/code&gt;, and if you want the MCP server too, &lt;code&gt;composer require laravel/mcp&lt;/code&gt; on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;More &lt;code&gt;truss:doctor&lt;/code&gt; rules and CI-native output formats are next on the &lt;a href="https://trussphp.com/roadmap/" rel="noopener noreferrer"&gt;roadmap&lt;/a&gt;, followed by reading Eloquent relationships for semantic edge labels instead of raw foreign keys, and navigation aids for schemas with a hundred tables or more. If a tool the agent needs isn't there yet, or an annotation source you'd want isn't supported, &lt;a href="https://github.com/albertoarena/laravel-truss/discussions" rel="noopener noreferrer"&gt;open a discussion&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>ai</category>
      <category>database</category>
    </item>
    <item>
      <title>Claude Code Auto Mode: What Still Needs a Human</title>
      <dc:creator>Alberto Arena</dc:creator>
      <pubDate>Mon, 10 Aug 2026 08:43:01 +0000</pubDate>
      <link>https://dev.to/alberto_arena/claude-code-auto-mode-what-still-needs-a-human-kg5</link>
      <guid>https://dev.to/alberto_arena/claude-code-auto-mode-what-still-needs-a-human-kg5</guid>
      <description>&lt;p&gt;Starting August 14, &lt;a href="https://claude.com/blog/auto-mode-default-in-claude-code" rel="noopener noreferrer"&gt;auto mode becomes the default permission mode&lt;/a&gt; for Claude Code on Pro, Max, and Team plans. Buried in Anthropic's own announcement is a number worth sitting with: in a controlled test with 1,053 paid professional testers, researchers swapped in genuinely dangerous commands partway through real sessions. Humans caught 13.6% of them. Auto mode's classifier caught 89%.&lt;/p&gt;

&lt;p&gt;That's not a subtle gap, and reading it made me a little uncomfortable, because I've spent most of the last few months running my own sessions in accept-edits or auto mode, clicking through permission prompts the same way everyone apparently does. Anthropic's own usage data backs that up: people approve 97% of ordinary permission prompts, and the rate at which someone actually catches a dangerous one degrades from roughly 17% early in a session to about 5% after fifty-plus prior approvals. We're not reading carefully. We're pattern-matching on "this looks like the last forty things I clicked yes on," and that pattern breaks exactly when it stops holding.&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;Human review&lt;/th&gt;
&lt;th&gt;Auto mode classifier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dangerous commands caught (1,053-tester study)&lt;/td&gt;
&lt;td&gt;13.6%&lt;/td&gt;
&lt;td&gt;89%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production-severity unintended harm, real flagged sessions&lt;/td&gt;
&lt;td&gt;6.3%&lt;/td&gt;
&lt;td&gt;2.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second row comes from Anthropic's audit of real, already-flagged sessions rather than a lab test: manually-approved sessions contained serious unintended harm more than twice as often as auto mode sessions did. Different methodology, same direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changes on August 14
&lt;/h2&gt;

&lt;p&gt;Auto mode itself is a fairly simple change to the default flow. Right now, Claude pauses before most file edits and shell commands and waits for you to click approve. Under auto mode, it doesn't. Instead, a separate classifier model reviews each action in the background as Claude proposes it, and only interrupts you when it judges something risky: an irreversible action, a request that escalates beyond what you asked for, an attempt to send data somewhere it shouldn't go. Everything else, the reads, the routine edits, the ordinary shell commands, runs without a prompt at all.&lt;/p&gt;

&lt;p&gt;The classifier sits underneath the permission mode you've already picked, not instead of it. Your allow, ask, and deny rules resolve first; the classifier only gets a say on whatever those rules don't already settle. That layering is exactly what makes the rest of this post possible: the classifier isn't the only thing standing between Claude and a bad command, it's the last one, and everything upstream of it is something you configure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The asterisk Anthropic puts on its own number
&lt;/h2&gt;

&lt;p&gt;None of this means auto mode is safe, and Anthropic doesn't claim it is. Their own line: "auto mode relies on classification systems and therefore does not eliminate risk." They ran a two-week red-teaming pilot with &lt;a href="https://watcher.apolloresearch.ai/" rel="noopener noreferrer"&gt;Apollo Research&lt;/a&gt;, injecting synthetic attacks into real workflows. Before the hardening that shipped with this release, the classifier missed 12% of those attacks. After hardening, 7%, on a test set the classifier had never seen during tuning. Better than a distracted human. Not zero.&lt;/p&gt;

&lt;p&gt;What the classifier is actually built to catch is specific: scope escalation on a vague or destructive request, credential exploration, exfiltration through an external service like a public gist, retrying a failed deploy with a skip-verification flag, a command that reads normally but was seeded by hostile content Claude picked up along the way. Where it still misses isn't usually "is this dangerous", it correctly flags danger most of the time. It's "does a real consent signal in this session actually cover this specific action", the same ambiguity a tired human glosses over, just less often.&lt;/p&gt;

&lt;p&gt;So the useful question isn't auto mode versus manual review. It's what to put alongside auto mode that fails in a different way than auto mode does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules that hold no matter the mode
&lt;/h2&gt;

&lt;p&gt;The classifier isn't the only layer available, and it's not the one you have the most control over. Permission rules sit underneath it: &lt;a href="https://code.claude.com/docs/en/permission-modes" rel="noopener noreferrer"&gt;the docs are explicit&lt;/a&gt; that "these controls apply in every mode, including &lt;code&gt;bypassPermissions&lt;/code&gt;: deny rules and explicit ask rules." Deny is a hard stop the classifier never gets to weigh in on. Ask still forces a prompt, even in a mode whose entire point is skipping prompts, which matters because the example below relies on it.&lt;/p&gt;

&lt;p&gt;Here's a concrete gap worth knowing about: auto mode auto-approves pushes to any branch of the repository you're working in, including the default branch, unless you tell it otherwise. The classifier judges the push's content on its own terms, but the act of pushing to &lt;code&gt;main&lt;/code&gt; isn't itself something it blocks by default. If you want that to always stop for a human, write it down:&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;"permissions"&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;"ask"&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="s2"&gt;"Bash(git push origin main)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git push origin master)"&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;The push example above uses &lt;code&gt;ask&lt;/code&gt; on purpose: you still want to push eventually, just with a human in the loop first. That's different from an invariant you never want crossed at all, "no data exposed, ever," say, in a &lt;code&gt;CLAUDE.md&lt;/code&gt; file. A rules file states that kind of invariant as a request Claude tries to honor. A deny rule enforces it: a request the tool can't route around, in any mode, on any day you forget you're in auto mode at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  A gate automation can't talk itself out of
&lt;/h2&gt;

&lt;p&gt;Rules cover what you can name in advance. Hooks cover what you want checked every time, mechanically, regardless of what the classifier decides. A &lt;code&gt;PreToolUse&lt;/code&gt; hook can inspect a tool call before it runs and block it outright, deterministically, with no probability involved:&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;"hooks"&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;"PreToolUse"&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="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"if"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git push *)"&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;"${CLAUDE_PROJECT_DIR}/.claude/hooks/require-green-tests.sh"&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;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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# .claude/hooks/require-green-tests.sh&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; npm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--silent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Tests are red, push blocked"}}'&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;if&lt;/code&gt; field narrows this to &lt;code&gt;git push&lt;/code&gt; calls specifically, the script runs the test suite, and a failing suite returns a &lt;code&gt;deny&lt;/code&gt; decision that blocks the push outright, exit code 2 works the same way with a plain stderr message. Nothing here asks a classifier to judge intent. It's a yes/no gate on a fact you can check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review moves, it doesn't disappear
&lt;/h2&gt;

&lt;p&gt;None of the above replaces reading the diff. It changes what "reading the diff" is for. &lt;a href="https://dev.to/posts/we-became-editors-in-chief/"&gt;I wrote before about how agentic development turned most of the job into review&lt;/a&gt;, and the failure mode there isn't that people stopped reviewing, it's that review quality degrades under volume exactly the way the approval-fatigue numbers above describe. Auto mode doesn't fix that. It just removes the low-value prompts so the review that's left can, in theory, be the kind that actually catches something.&lt;/p&gt;

&lt;p&gt;In practice that means picking one point in the loop where you deliberately stay the bottleneck, on purpose, and the push to a shared branch is the obvious candidate. It's the one action that's genuinely hard to undo and visible to someone else the moment it happens. Everything upstream of that, a wrong edit, a bad approach, a half-finished refactor, is still local and still reversible. &lt;a href="https://dev.to/posts/ai-hallucination-in-coding-agents/"&gt;Hallucinated code compiles and passes a quick glance&lt;/a&gt;; it doesn't survive someone actually reading it against what they asked for. That reading has to happen somewhere. Put it at the boundary that matters and let the classifier and the hooks handle the volume everywhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests are the check that doesn't get tired
&lt;/h2&gt;

&lt;p&gt;The hook above only works because there's a test suite behind it. That's the other half of the strategy: tests are the executable version of "did this actually do what I asked", and unlike a human on prompt four hundred of a session, they don't get less careful over time. Write them before or alongside the change, not after, and the &lt;code&gt;git push&lt;/code&gt; gate above stops being a formality and starts being the thing that actually catches a regression a tired reviewer would have waved through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stacking checks that fail differently
&lt;/h2&gt;

&lt;p&gt;None of this makes the miss rate zero, same as auto mode itself doesn't. The point isn't finding the one control that eliminates risk. It's stacking controls that fail in different ways, so the same blind spot doesn't line up twice. The classifier catches the obvious and the fast. Deny rules catch the specific thing you already know matters. Hooks catch what you can specify as a check. Tests catch what you can assert. And a deliberate human read at the point something becomes shared catches the one thing none of the above can judge: whether this was the right thing to build in the first place.&lt;/p&gt;

&lt;p&gt;Thirteen point six percent is a bad number for humans working alone. Eighty-nine percent is a good number for a classifier working alone. Neither one is the number that matters. The number that matters is what's left over once you've stacked both, plus the checks that are actually yours to configure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://claude.com/blog/auto-mode-default-in-claude-code" rel="noopener noreferrer"&gt;Auto mode is now the default in Claude Code for Pro, Max, and Team plans&lt;/a&gt;: the announcement this post responds to, including the 1,053-tester study and the real-session audit numbers.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.anthropic.com/engineering/claude-code-auto-mode" rel="noopener noreferrer"&gt;How we built Claude Code auto mode: a safer way to skip permissions&lt;/a&gt;: the technical writeup covering the classifier's two-stage design and what it's built to catch.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://code.claude.com/docs/en/permission-modes" rel="noopener noreferrer"&gt;Choose a permission mode&lt;/a&gt;: official documentation on &lt;code&gt;default&lt;/code&gt;, &lt;code&gt;acceptEdits&lt;/code&gt;, &lt;code&gt;bypassPermissions&lt;/code&gt;, and how permission rules interact with each mode.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;Hooks reference&lt;/a&gt;: official documentation on hook events, matcher syntax, and the &lt;code&gt;PreToolUse&lt;/code&gt; block/allow contract used in the example above.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <item>
      <title>I built a Laravel event-sourcing generator, then the AI version</title>
      <dc:creator>Alberto Arena</dc:creator>
      <pubDate>Thu, 06 Aug 2026 06:01:00 +0000</pubDate>
      <link>https://dev.to/alberto_arena/i-built-a-laravel-event-sourcing-generator-then-the-ai-version-422i</link>
      <guid>https://dev.to/alberto_arena/i-built-a-laravel-event-sourcing-generator-then-the-ai-version-422i</guid>
      <description>&lt;p&gt;A while back I built a &lt;a href="https://dev.to/posts/laravel-event-sourcing-generator-10k/"&gt;code generator for event sourcing in Laravel&lt;/a&gt;: run one artisan command against &lt;code&gt;spatie/laravel-event-sourcing&lt;/code&gt;, get aggregates, events, projectors, and reactors scaffolded out. It's deterministic. Same inputs, same output, every time. It just crossed 10,000 downloads on Packagist.&lt;/p&gt;

&lt;p&gt;More recently I built a &lt;a href="https://dev.to/posts/ai-laravel-event-sourcing/"&gt;Claude Code skill&lt;/a&gt; that produces the same kind of domain code, except it starts with a conversation instead of a command. So the question people keep sending me is obvious: does the AI skill replace the generator?&lt;/p&gt;

&lt;p&gt;No. And the reason is more interesting than the question.&lt;/p&gt;

&lt;p&gt;The short version, if you skip everything else: AI didn't kill the code generator. It took over the &lt;em&gt;design&lt;/em&gt; half of the job and left the deterministic half exactly where determinism still matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the generator is good at
&lt;/h2&gt;

&lt;p&gt;The generator's whole value proposition is determinism. &lt;code&gt;composer require albertoarena/laravel-event-sourcing-generator&lt;/code&gt;, then &lt;code&gt;php artisan make:event-sourcing-domain Order&lt;/code&gt;, and you get exactly the files you'd expect, byte-identical every run. No tokens, no API key, no network dependency, nothing to review for "did it hallucinate a method." It runs the same in CI as it does on my laptop.&lt;/p&gt;

&lt;p&gt;That's exactly right when I already know the shape of the domain: I know the aggregate boundary, I know the events, I just need the boilerplate written consistently. What it can't do is help me figure out whether that boundary is correct in the first place. It assumes I already did the hard part.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the skill is good at
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/albertoarena/claude-laravel-event-sourcing" rel="noopener noreferrer"&gt;skill&lt;/a&gt; is not a code generator wearing an AI costume. Its value sits upstream of the code, in a two-gate workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gate 1, design.&lt;/strong&gt; It asks focused questions about the domain and produces an Architecture Decision Record: aggregates, commands, events, projectors, reactors, and the invariants holding them together. Nothing gets written until I approve that ADR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gate 2, implementation.&lt;/strong&gt; Once I approve it, it writes tests first, then generates the full domain, and runs the suite. For that &lt;code&gt;cancel&lt;/code&gt;-after-&lt;code&gt;ship&lt;/code&gt; rule, that means a test asserting the aggregate throws before a line of the aggregate itself exists, not after.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The part that actually matters is Gate 1. "Can &lt;code&gt;cancel&lt;/code&gt; still fire after &lt;code&gt;ship&lt;/code&gt;?" is a modelling question, and that's the conversation where event sourcing lives or dies. The generator was never going to have that conversation with me. It doesn't ask questions; it executes a decision I've already made.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real dividing line
&lt;/h2&gt;

&lt;p&gt;Both tools end up emitting similar-looking PHP, which is why people fixate on the overlap. But codegen is the least interesting thing either one does. The line that actually matters is this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I already know the design → the generator.&lt;/strong&gt; Deterministic, fast, no review tax, no dependency on an LLM being available or correct.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I'm still figuring out the design → the skill.&lt;/strong&gt; It's a modelling partner that leaves a paper trail (the ADR) and a safety net (tests written before the implementation exists).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which one I actually reach for
&lt;/h2&gt;

&lt;p&gt;Honestly, mostly the skill these days. Most of the time I sit down to add a new bounded context, the boundary isn't fully settled yet, and paying for a five-minute design conversation before anything gets written is worth more than skipping straight to boilerplate. The ADR forces me to answer the awkward "wait, should that be an event or a side effect?" questions before they turn into a rewrite.&lt;/p&gt;

&lt;p&gt;That doesn't mean the generator's properties stopped mattering. It still gets pulled out whenever the reasons I built it in the first place are the reasons that matter: expanding a domain whose shape is already locked in, running in a pipeline where I don't want an LLM in the loop, or anywhere determinism and zero token cost outrank a design conversation I don't need to have again. I just don't hit that situation as often as I used to, now that the skill exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I don't use the skill at all
&lt;/h2&gt;

&lt;p&gt;Being upfront about the edges is the point of writing this. The skill is greenfield only. It designs new event-sourced domains; it does not refactor existing CRUD into event sourcing, and it's scoped specifically to &lt;code&gt;spatie/laravel-event-sourcing&lt;/code&gt; v7, not Laravel's own event system, not CQRS without event sourcing, not other packages. If that's your situation, neither tool here is the answer, and I'd rather say so than sell you a mismatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try either
&lt;/h2&gt;

&lt;p&gt;If you're modelling a new event-sourced domain and Claude Code is already part of your workflow, the skill installs in two steps: the first points Claude Code at the marketplace (nothing installs yet), the second installs the plugin from it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add albertoarena/claude-laravel-event-sourcing
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;laravel-spatie-event-sourcing@albertoarena
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you already know the shape of the domain and just want the boilerplate, the generator is a Composer install away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require albertoarena/laravel-event-sourcing-generator
php artisan make:event-sourcing-domain Order
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full write-ups of each, if you want the mechanics: &lt;a href="https://dev.to/posts/laravel-event-sourcing-generator-10k/"&gt;the generator&lt;/a&gt;, &lt;a href="https://dev.to/posts/ai-laravel-event-sourcing/"&gt;the skill&lt;/a&gt;, and the &lt;a href="https://dev.to/posts/domain-using-spatie-event-sourcing/"&gt;original walkthrough of building a domain with Spatie event sourcing&lt;/a&gt; if you're new to any of this.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>laravel</category>
      <category>php</category>
      <category>code</category>
    </item>
    <item>
      <title>I gave my schema viewer your app's colours</title>
      <dc:creator>Alberto Arena</dc:creator>
      <pubDate>Tue, 04 Aug 2026 11:25:25 +0000</pubDate>
      <link>https://dev.to/alberto_arena/i-gave-my-schema-viewer-your-apps-colours-1nhd</link>
      <guid>https://dev.to/alberto_arena/i-gave-my-schema-viewer-your-apps-colours-1nhd</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/albertoarena/laravel-truss" rel="noopener noreferrer"&gt;Truss&lt;/a&gt; is a live, zoomable ER diagram of your Laravel app's actual database schema, safe to run in production because it only ever reads structure. I introduced it &lt;a href="https://dev.to/posts/introducing-truss/"&gt;back in July&lt;/a&gt;, and it's been part of every client project since, gated behind a &lt;code&gt;viewTruss&lt;/code&gt; policy. Two things kept nagging at me. The dashboard was always Truss blue, whatever the client's actual brand was, so it looked bolted onto their app rather than part of it. And the export button, one click for a Markdown data dictionary or a DBML file, only ever got clicked when I remembered to, so the schema file committed to the repo drifted quietly from the real one until someone noticed, usually the hard way.&lt;/p&gt;

&lt;p&gt;Truss 1.6 fixes both: a themeable dashboard that matches whatever it's embedded in, and a &lt;code&gt;truss:export&lt;/code&gt; command that puts the same output in your terminal and your CI pipeline, no browser required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Match it to your app
&lt;/h2&gt;

&lt;p&gt;Colours and fonts now live under &lt;code&gt;truss.theme&lt;/code&gt; in config, and Truss re-skins itself in both light and dark mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/truss.php&lt;/span&gt;
&lt;span class="s1"&gt;'theme'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'colors'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'light'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'accent'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'#b45309'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'accent-secondary'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'#0f766e'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'background'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'#faf6f0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'surface'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'#fffdf8'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'text'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'#3a2a1c'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'border'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'#c8873f'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'dark'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'accent'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'#fbbf24'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'background'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'#1b130b'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'surface'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'#26190d'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'text'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'#f5e6d0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'border'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'#8a6428'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&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;Only the knobs you set are overridden, everything else stays on the default Blueprint palette, so three or four values are enough to make it look at home. The re-skin runs deep: the diagram's relationship lines, label backdrops, background grid, table rows, and inputs all follow, not just the toolbar chrome. It ships as a same-origin stylesheet, no build step, &lt;code&gt;style-src 'self'&lt;/code&gt; still covers it, and a default install adds no extra request at all. Each value is validated before it's written, so a typo falls back to the default instead of breaking the sheet.&lt;/p&gt;

&lt;p&gt;Picking hex values by hand isn't required. The new &lt;a href="https://trussphp.com/theme-builder/" rel="noopener noreferrer"&gt;theme builder&lt;/a&gt; previews a palette against the real dashboard live, starts from presets (Blueprint, Ember, Contrast) if you'd rather tweak than start blank, and generates the exact &lt;code&gt;config/truss.php&lt;/code&gt; snippet to paste in.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffaz9xz7rdz8detx0ay4b.webp" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffaz9xz7rdz8detx0ay4b.webp" alt="The Truss theme builder in light mode: colour knobs on the left, the schema diagram re-skinned in a warm amber and teal palette on the right" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Get a schema file without opening a browser
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan truss:export &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dbml &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docs/schema.dbml
php artisan truss:export &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json &lt;span class="nt"&gt;--check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;truss:export&lt;/code&gt; is the export button's command-line counterpart: the same five formats (DBML, JSON, CSV, Markdown data dictionary, Mermaid), generated straight from PHP, no browser involved. Output is deterministic, the same schema always produces the same bytes, which is what makes &lt;code&gt;--check&lt;/code&gt; worth running: point it at the file you've committed, and it exits &lt;code&gt;1&lt;/code&gt; the moment that file stops matching the database, &lt;code&gt;2&lt;/code&gt; on a usage error, &lt;code&gt;0&lt;/code&gt; when everything lines up. Wire it into a commit hook or CI, and schema drift stops being something a reviewer has to notice by eye. Structure only, as always, no network call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Theme builder: &lt;a href="https://trussphp.com/theme-builder/" rel="noopener noreferrer"&gt;trussphp.com/theme-builder&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Live demo: &lt;a href="https://trussphp.com/demo/" rel="noopener noreferrer"&gt;trussphp.com/demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Theming guide: &lt;a href="https://trussphp.com/guides/theming/" rel="noopener noreferrer"&gt;trussphp.com/guides/theming&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Schema export guide: &lt;a href="https://trussphp.com/guides/schema-export/" rel="noopener noreferrer"&gt;trussphp.com/guides/schema-export&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Release notes: &lt;a href="https://github.com/albertoarena/laravel-truss/releases/tag/v1.6.0" rel="noopener noreferrer"&gt;v1.6.0 on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Update with &lt;code&gt;composer update albertoarena/laravel-truss&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Lighthouse CI is next on the &lt;a href="https://trussphp.com/roadmap/" rel="noopener noreferrer"&gt;roadmap&lt;/a&gt;: automated performance and accessibility audits across both themes, light and dark. After that: more &lt;code&gt;truss:doctor&lt;/code&gt; rules, reading Eloquent relationships for semantic labels instead of just foreign keys, and schema context formatted for AI agents working in the same codebase. If a theme knob is missing or &lt;code&gt;truss:export&lt;/code&gt; should support a format it doesn't, &lt;a href="https://github.com/albertoarena/laravel-truss/discussions" rel="noopener noreferrer"&gt;open a discussion&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>database</category>
      <category>opensource</category>
    </item>
    <item>
      <title>We Became Editors-in-Chief, and Nobody Trained Us</title>
      <dc:creator>Alberto Arena</dc:creator>
      <pubDate>Mon, 03 Aug 2026 06:13:59 +0000</pubDate>
      <link>https://dev.to/alberto_arena/we-became-editors-in-chief-and-nobody-trained-us-2j49</link>
      <guid>https://dev.to/alberto_arena/we-became-editors-in-chief-and-nobody-trained-us-2j49</guid>
      <description>&lt;p&gt;Most of my working day now looks like this: I write a prompt describing what I want. An agent produces a diff. I read it, and either approve it or ask for a refinement. Repeat, dozens of times, until the feature is done.&lt;/p&gt;

&lt;p&gt;Ask whether that makes us "prompt engineers" and you're asking the wrong question. Prompt wording is a thin skill, learnable in an afternoon. What's actually changed, after twenty-seven years of writing code the old way, is what the job does to your attention. Less typing. Much more reviewing, at a volume and pace nothing in my career prepared me for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop, honestly described
&lt;/h2&gt;

&lt;p&gt;Strip away the branding and agentic development is a review cycle. You describe an outcome. The agent proposes an implementation. You check it against a mental model of what "correct" means for this codebase, this edge case, this architecture, and you either accept it or push back with more context.&lt;/p&gt;

&lt;p&gt;That's not a new activity. It's code review, except the author on the other side never gets tired, never pushes back, and produces a plausible-looking diff in seconds whether it's right or not. The volume changes everything. When review is the bottleneck instead of typing, the job's centre of gravity moves with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A different kind of tired
&lt;/h2&gt;

&lt;p&gt;The old way was stressful in bursts. You'd hit a bug, lose an afternoon to it, feel the specific frustration of a stack trace that refuses to make sense. Stressful, but the pace was yours. Typing is slow. Debugging forces pauses. Whether you wanted it or not, there was slack built into the rhythm.&lt;/p&gt;

&lt;p&gt;Agentic coding removes that slack. The agent doesn't pace itself to your attention span. It just keeps producing diffs, one after another, faster than you'd ever type them yourself. To review well you need prolonged, focused attention, held at a level that used to only show up during the hard parts. There's no natural lull left for your brain to reset in, because generation never slows down to let you catch up. A whole afternoon of that is a different kind of tired than a whole afternoon spent stuck on one stubborn bug, and after twenty-seven years of the old rhythm, this one still catches me off guard some days.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Prompt engineer" is the wrong name for it
&lt;/h2&gt;

&lt;p&gt;The skill that matters here was never prompt wording. It's knowing what correct looks like before you see the diff: why a query needs an index, why a migration is unsafe under concurrent writes, why a "working" abstraction will hurt in six months. That judgment doesn't come from prompting practice. It comes from having built things, broken things, and paid for the mistakes.&lt;/p&gt;

&lt;p&gt;The people getting the most out of agentic tools right now aren't the ones with the cleverest prompts. They're the ones who can look at generated code and immediately spot the one line that's subtly wrong, because they've written that exact bug themselves at some point. Prompting is the interface. Engineering judgment is still the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes in five years
&lt;/h2&gt;

&lt;p&gt;Three things seem likely to me, in no particular order of confidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review becomes the skill we train for, not the one we tolerate.&lt;/strong&gt; Right now most of us learned to review code as a secondary skill, something you did after learning to write it. If specifying and judging becomes the primary loop, reviewing well, fast and without rubber-stamping, has to become something we deliberately get better at, the way we once deliberately got better at algorithms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We start designing around reviewer fatigue, not just reviewer speed.&lt;/strong&gt; Right now most tooling optimises for how fast an agent can produce a diff. The actual constraint is how long a human can sustain high-attention review before quality drops. I'd expect smaller, more frequent checkpoints, better summaries of what changed and why before you dive into the diff itself, and more deliberate pacing, because burning out the reviewer is now the real bottleneck, not the code generation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The gap between good and bad engineers widens, not narrows.&lt;/strong&gt; Agentic tools are a multiplier, and multipliers amplify what's already there. An engineer with strong judgment reviews faster and catches more. An engineer without it approves more bugs, faster than before. The floor for "can produce working code" drops. The ceiling for "can be trusted to ship it" doesn't move, or gets more valuable precisely because it's rarer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I'm less sure about
&lt;/h2&gt;

&lt;p&gt;What worries me isn't the loop itself: it's whether the judgment that makes the loop work survives if fewer people spend years building it the hard way. Review is only as good as the model you're reviewing against, and that model used to be built by writing thousands of lines yourself, badly, and fixing them. If the next generation of engineers reviews before it ever really writes, I don't know yet whether that produces the same instincts faster, or a generation of reviewers who can't tell a plausible diff from a correct one.&lt;/p&gt;

&lt;p&gt;So no, I don't think we're becoming prompt engineers. I think we're becoming editors-in-chief of our own code, and editing well is its own craft, one most of us are still learning on the job. Where that leaves the next five years is the part I'm genuinely curious about, not the part I have a tidy answer for.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://albertoarena.it/posts/we-became-editors-in-chief/" rel="noopener noreferrer"&gt;albertoarena.it&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>productivity</category>
      <category>discuss</category>
    </item>
    <item>
      <title>The schema doctor is in</title>
      <dc:creator>Alberto Arena</dc:creator>
      <pubDate>Fri, 31 Jul 2026 05:05:07 +0000</pubDate>
      <link>https://dev.to/alberto_arena/the-schema-doctor-is-in-i8k</link>
      <guid>https://dev.to/alberto_arena/the-schema-doctor-is-in-i8k</guid>
      <description>&lt;p&gt;A migration that adds a &lt;code&gt;user_id&lt;/code&gt; column with no supporting index passes review every time. It looks fine. Laravel doesn't complain, the migration runs clean, CI is green. Then a few months later someone's staring at a JOIN that touches two hundred rows and takes eight seconds, and nobody remembers why.&lt;/p&gt;

&lt;p&gt;That's the class of problem &lt;a href="https://github.com/albertoarena/laravel-truss" rel="noopener noreferrer"&gt;Truss&lt;/a&gt; 1.5 is built to catch before it ships: the schema doctor.&lt;/p&gt;

&lt;p&gt;I wrote about &lt;a href="https://dev.to/posts/introducing-truss/"&gt;Truss&lt;/a&gt; here in July, a live, zoomable ER diagram of your Laravel app's actual database schema. It's picked up more traction than I expected since then: it's closing in on 300 installs on Packagist and past 70 stars on GitHub, almost entirely word of mouth in the Laravel community. That traction is exactly what pushed the next feature: a diagram is great for seeing your schema, but it doesn't tell you when something in it is wrong. &lt;code&gt;truss:doctor&lt;/code&gt; does.&lt;/p&gt;

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

&lt;p&gt;Run it and it flags problems that are visible from structure alone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tables with no primary key&lt;/li&gt;
&lt;li&gt;Foreign keys with no supporting index&lt;/li&gt;
&lt;li&gt;Duplicate or redundant indexes&lt;/li&gt;
&lt;li&gt;Foreign keys whose type doesn't match the key they reference&lt;/li&gt;
&lt;li&gt;Money-looking columns stored as &lt;code&gt;float&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Pivots without a unique key, unindexed &lt;code&gt;deleted_at&lt;/code&gt;, and more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thirteen rules in total, across integrity, index, and type categories, each with a stable code (like &lt;code&gt;TRUSS-IDX-001&lt;/code&gt;) so you can look it up, silence it, or change its severity.&lt;/p&gt;

&lt;p&gt;Rules are engine-aware where it matters: an unindexed foreign key is an &lt;strong&gt;error&lt;/strong&gt; on PostgreSQL and SQLite, but only &lt;strong&gt;info&lt;/strong&gt; on MySQL and MariaDB, since those auto-index foreign keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  In the terminal and CI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan truss:doctor
php artisan truss:doctor &lt;span class="nt"&gt;--preset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;strict &lt;span class="nt"&gt;--fail-on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;warning
php artisan truss:doctor &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It exits non-zero once a finding hits your fail level, so a migration that introduces a problem fails the build. No AI, no queries, no row data: deterministic and structure-only, safe to run in a commit hook or a CI pipeline against a database you don't fully trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  In the dashboard
&lt;/h2&gt;

&lt;p&gt;The same findings surface in the dashboard as a new Health panel, the heart icon in the toolbar. Tables with a problem are flagged right on the diagram, the offending column is marked inline, and lower-confidence (heuristic) findings are labelled as such. Same engine, same findings, two front ends depending on whether you're at a terminal or in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://trussphp.com/demo/" rel="noopener noreferrer"&gt;live demo&lt;/a&gt; has it running against a fictional schema. Open the Health panel and you'll see a handful of foreign keys flagged for a missing index.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live demo: &lt;a href="https://trussphp.com/demo/" rel="noopener noreferrer"&gt;trussphp.com/demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Guide, with every rule code: &lt;a href="https://trussphp.com/guides/schema-doctor/" rel="noopener noreferrer"&gt;trussphp.com/guides/schema-doctor&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Release notes: &lt;a href="https://github.com/albertoarena/laravel-truss/releases/tag/v1.5.0" rel="noopener noreferrer"&gt;v1.5.0 on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Update with &lt;code&gt;composer update albertoarena/laravel-truss&lt;/code&gt;, then run &lt;code&gt;php artisan truss:doctor&lt;/code&gt; and see what's already in there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;This is phase one: the engine and the thirteen rules. Suppression files, more rules, and CI formatters are on the &lt;a href="https://trussphp.com/roadmap/" rel="noopener noreferrer"&gt;roadmap&lt;/a&gt;. If a rule feels too noisy, too quiet, or there's a check you'd want that isn't there yet, the &lt;a href="https://github.com/albertoarena/laravel-truss/discussions/18" rel="noopener noreferrer"&gt;discussion thread&lt;/a&gt; is open. I'd love to hear what you run it against.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>database</category>
      <category>opensource</category>
    </item>
    <item>
      <title>CLAUDE.md Is RAM, Skills Are Not Disk: The Four-Tier Memory Model for Claude Code</title>
      <dc:creator>Alberto Arena</dc:creator>
      <pubDate>Mon, 27 Jul 2026 10:30:21 +0000</pubDate>
      <link>https://dev.to/alberto_arena/claudemd-is-ram-skills-are-not-disk-the-four-tier-memory-model-for-claude-code-1cp8</link>
      <guid>https://dev.to/alberto_arena/claudemd-is-ram-skills-are-not-disk-the-four-tier-memory-model-for-claude-code-1cp8</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdbpoxacgegg33fz9gl8p.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdbpoxacgegg33fz9gl8p.jpg" alt="CLAUDE.md Is RAM, Skills Are Not Disk: The Four-Tier Memory Model for Claude Code" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code loads &lt;code&gt;CLAUDE.md&lt;/code&gt; in full at the start of every session and keeps it resident the whole time, so every line in it is a line you pay for on every turn, whether the task in front of you needs it or not. &lt;a href="https://dev.to/posts/claude-md-is-ram-not-disk/"&gt;CLAUDE.md Is RAM, Not Disk&lt;/a&gt; split that into two tiers to deal with it: &lt;code&gt;CLAUDE.md&lt;/code&gt; holds only what Claude needs every time, stack, commands, invariants; everything else moves to &lt;code&gt;docs/&lt;/code&gt;, which loads only when something points Claude there. Working memory stays small and constantly paid for. Long-term memory stays cheap because it's optional.&lt;/p&gt;

&lt;p&gt;I wasn't using custom skills when I wrote that post. Once I started, the obvious move was to file them under disk too: they're detail Claude doesn't need every turn, so surely they belong wherever the rest of the on-demand material lives. That reasoning is wrong, and it took checking how skills actually behave in context to see why. A skill is not a document Claude opens when curious. It's closer to a program that gets launched, and treating it like disk is how invariants end up sitting behind a trigger that might not fire.&lt;/p&gt;

&lt;p&gt;Here's the model with two more tiers than it had before: &lt;code&gt;CLAUDE.md&lt;/code&gt; is RAM, path-scoped rules are demand-paged RAM, skills are installed programs, and &lt;code&gt;docs/&lt;/code&gt; is disk. Each tier answers the same question differently: when does this load, and what happens to it after. Get that question right for a given piece of instruction and the tier picks itself.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Checked against Claude Code v2.1.212 on July 27, 2026. The listing and compaction budgets below are the kind of number that moves; verify them against the current docs before leaning on the exact figures.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the two-tier model couldn't see
&lt;/h2&gt;

&lt;p&gt;The original test was: does Claude need this every turn, or only sometimes? That question sorts &lt;code&gt;CLAUDE.md&lt;/code&gt; from &lt;code&gt;docs/&lt;/code&gt; correctly, but it quietly assumes there are only two ways for something to load: resident from the start, or resident only once Claude decides to open a file. Path-scoped rules and skills both load conditionally, and neither one is "Claude decides to open a file."&lt;/p&gt;

&lt;p&gt;A rule loads because a file path matched a glob. That's deterministic, the same way session start is deterministic, just scoped instead of constant. A skill loads because Claude matched a task description to a name in a list. That's probabilistic: it matches text, so it usually fires and sometimes does not. Collapsing those two into "sometimes" and filing both next to &lt;code&gt;docs/&lt;/code&gt; erases the one distinction that actually matters: whether the content is guaranteed to show up when you need it, or merely likely to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tier 1: RAM, unchanged
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; still works exactly the way the first post described it: loaded in full at session start, resident every turn after, re-read from disk and re-injected after &lt;code&gt;/compact&lt;/code&gt;. Adding three more tiers doesn't change what belongs here, stack, commands, the handful of invariants that must never break. It's still the only tier with no "when does this load" question to answer. The answer is always.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tier 2: demand-paged RAM, and a correction to the first post
&lt;/h2&gt;

&lt;p&gt;The nested &lt;code&gt;CLAUDE.md&lt;/code&gt; trick from the original post still works, but there's a cleaner mechanism for the same job. If you copied that trick into your own project, this is the point where I'd tell you to migrate it. &lt;code&gt;.claude/rules/*.md&lt;/code&gt; files with a &lt;code&gt;paths:&lt;/code&gt; glob in their frontmatter load into context the same way a nested &lt;code&gt;CLAUDE.md&lt;/code&gt; does, when Claude reads a file that matches. The docs say a nested &lt;code&gt;CLAUDE.md&lt;/code&gt; is not re-injected after &lt;code&gt;/compact&lt;/code&gt; and reloads the next time Claude touches that subdirectory. They don't say the same for rules, and my own testing suggests it behaves the same way.&lt;/p&gt;

&lt;p&gt;Leave off &lt;code&gt;paths:&lt;/code&gt; and none of this applies. A rule file without it loads at launch with the same priority as &lt;code&gt;.claude/CLAUDE.md&lt;/code&gt; itself, so &lt;code&gt;.claude/rules/testing.md&lt;/code&gt; with no frontmatter is tier 1 wearing a different folder, not tier 2. The mechanism doesn't decide the tier. The loading behavior does.&lt;/p&gt;

&lt;p&gt;What a scoped rule adds over the nested &lt;code&gt;CLAUDE.md&lt;/code&gt; it replaces: the glob cuts across the directory tree instead of being pinned to one folder, so "every &lt;code&gt;*.php&lt;/code&gt; file under &lt;code&gt;app/Domains&lt;/code&gt;, but not the tests next door" is one line, instead of a rule you cannot express. Every rule file also lives in one folder instead of being scattered one per directory, so reviewing what conditional context exists in a project is a single &lt;code&gt;ls&lt;/code&gt;, not a find across the tree. Same loading behavior as before, better shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app/Domains/**/*.php"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Domain layer rules&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tier 3: installed programs
&lt;/h2&gt;

&lt;p&gt;A skill's name and description sit in a listing Claude can see from the start of the session, and that listing has its own budget: roughly 1% of the context window. When it overflows, Claude Code shortens descriptions starting with the skills you invoke least, so the ones you use constantly keep their full text and a skill you just added competes for space with everything else you've built. None of that costs much, which is the entire pitch: a project can carry thirty skills for a fraction of what thirty paragraphs in &lt;code&gt;CLAUDE.md&lt;/code&gt; would cost, as long as most of them don't fire in most sessions.&lt;/p&gt;

&lt;p&gt;The moment one does fire, that pitch stops applying. The full &lt;code&gt;SKILL.md&lt;/code&gt; body enters context and stays resident for the rest of the session, at the same per-turn cost as anything in &lt;code&gt;CLAUDE.md&lt;/code&gt;. Compaction treats it better than you'd expect but worse than the root file: the first 5,000 tokens of each invoked skill survive &lt;code&gt;/compact&lt;/code&gt;, shared across a 25,000 token combined budget, filled starting from whichever skill fired most recently. Invoke enough skills in one long session and the ones you called early get dropped entirely, quietly, with no warning that they're gone.&lt;/p&gt;

&lt;p&gt;Two things make a skill something other than either RAM or disk. It can bundle scripts that run without their code ever entering context, only the output does, which nothing in &lt;code&gt;CLAUDE.md&lt;/code&gt; or &lt;code&gt;docs/&lt;/code&gt; can do. And you can turn off its automatic trigger with &lt;code&gt;disable-model-invocation: true&lt;/code&gt;, so it only runs when you type &lt;code&gt;/name&lt;/code&gt;, trading the "maybe it fires" problem for "you have to remember to ask." That flag does more than disable auto-firing: it pulls the name and description out of the listing entirely, not just out of the trigger, so a skill set this way is the one case that costs nothing at all until you invoke it. That second point cuts both ways: a skill can also grant itself tool access through &lt;code&gt;allowed-tools&lt;/code&gt;, which makes a committed project skill something you review the way you'd review a settings file, not something you skim the way you'd skim a doc. Skills accept a &lt;code&gt;paths:&lt;/code&gt; glob too, the same format as rules, but it only narrows when Claude considers firing one automatically, so a scoped skill is filtered, not guaranteed.&lt;/p&gt;

&lt;p&gt;Skills are not disk, because disk is passive. A skill is an installed program with an entry in a lookup table, dormant until something calls it, and then it runs. That's also why the failure mode is specific. Putting a procedure behind a skill trigger is fine, because the cost of it not firing once is that Claude does the steps manually instead of running the script. Putting an invariant behind a skill trigger is not fine, because the cost of it not firing once is that the rule silently didn't apply, and a fuzzy description match is a bad place to gamble something that must always hold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tier 4: disk, unchanged
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;docs/&lt;/code&gt; still works exactly the way the first post described it. &lt;code&gt;DESIGN.md&lt;/code&gt;, &lt;code&gt;PLAN.md&lt;/code&gt;, &lt;code&gt;DECISIONS.md&lt;/code&gt;, long prose that humans read too, loaded only when something points to it and only because Claude decided to follow the pointer. It's still the weakest guarantee of the four tiers, and that's correct: disk was always supposed to be the tier you fall back to for material that doesn't need to be certain, only available.&lt;/p&gt;

&lt;p&gt;One trap is worth closing here, since it's the most common way people think they're on disk when they aren't. &lt;code&gt;@docs/DESIGN.md&lt;/code&gt; inside a &lt;code&gt;CLAUDE.md&lt;/code&gt; is not a pointer, it's an import: the file is expanded and loaded into context at launch, same as everything else in the root file. A plain prose reference, &lt;code&gt;docs/DESIGN.md&lt;/code&gt;, no &lt;code&gt;@&lt;/code&gt;, is what actually stays on disk until Claude follows it. The first post used prose pointers throughout, which was the right call, more by luck than by design.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four tiers side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Loaded when&lt;/th&gt;
&lt;th&gt;Resident cost&lt;/th&gt;
&lt;th&gt;Survives &lt;code&gt;/compact&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. RAM&lt;/td&gt;
&lt;td&gt;Root &lt;code&gt;CLAUDE.md&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Session start, in full&lt;/td&gt;
&lt;td&gt;Every turn&lt;/td&gt;
&lt;td&gt;Yes, re-read from disk and re-injected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Demand-paged RAM&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.claude/rules/*.md&lt;/code&gt; with &lt;code&gt;paths:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Claude reads a file matching the glob&lt;/td&gt;
&lt;td&gt;Every turn after that, for the rest of the session&lt;/td&gt;
&lt;td&gt;No, lost until Claude reads a matching file again (inferred, not documented)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Installed programs&lt;/td&gt;
&lt;td&gt;Skill in &lt;code&gt;.claude/skills/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Name and description always listed unless &lt;code&gt;disable-model-invocation&lt;/code&gt;; body loads on trigger&lt;/td&gt;
&lt;td&gt;Resident for the rest of the session once triggered&lt;/td&gt;
&lt;td&gt;First 5,000 tokens per skill, 25,000 combined, most recent first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Disk&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docs/*.md&lt;/code&gt; referenced by path (not &lt;code&gt;@&lt;/code&gt; imported)&lt;/td&gt;
&lt;td&gt;Only if Claude decides to read it&lt;/td&gt;
&lt;td&gt;Every turn after Claude reads it&lt;/td&gt;
&lt;td&gt;Whatever survives the summary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read top to bottom and it's also a certainty gradient. Tier 1 is guaranteed. Tier 2 is guaranteed for the files it's scoped to. Tier 3 is probable, or certain only if you invoke it yourself. Tier 4 is optional. Deciding where a piece of instruction belongs is really deciding how much certainty it needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applied to a Laravel and event sourcing project
&lt;/h2&gt;

&lt;p&gt;The starter repo from the first post had three tiers: root &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;docs/&lt;/code&gt;, and a nested &lt;code&gt;CLAUDE.md&lt;/code&gt; inside &lt;code&gt;app/Domains&lt;/code&gt;. It now has all four:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claude-code-laravel-starter/
├── CLAUDE.md                    # tier 1: stack, commands, invariants
├── README.md
├── .claude/
│   ├── rules/
│   │   └── domain.md            # tier 2: paths: ["app/Domains/**/*.php"]
│   └── skills/
│       └── README.md            # tier 3: how to install a project skill
├── docs/
│   ├── DESIGN.md                # tier 4: domain model, aggregates, events
│   ├── PLAN.md                  # tier 4: phased, plan-first implementation
│   └── DECISIONS.md             # tier 4: lightweight ADR log
└── app/
    └── Domains/                  # rules replace the nested CLAUDE.md that used to live here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;domain.md&lt;/code&gt; keeps the rule that used to live in the nested file: no framework imports in the domain layer, aggregates only record events, events are immutable and past tense. What changed is where it lives: one file in a folder you can review alongside every other conditional rule in the project, instead of a &lt;code&gt;CLAUDE.md&lt;/code&gt; you have to remember exists three directories down. &lt;code&gt;.claude/skills/&lt;/code&gt; doesn't ship a fake example skill; it documents how to install the real one, my &lt;a href="https://github.com/albertoarena/claude-laravel-event-sourcing" rel="noopener noreferrer"&gt;Laravel event sourcing skill&lt;/a&gt;, which designs and generates the aggregate, events, and projector for a new bounded context in conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four rules to take away
&lt;/h2&gt;

&lt;p&gt;If Claude needs it every turn, it goes in &lt;code&gt;CLAUDE.md&lt;/code&gt;. If it only matters inside files that match a pattern, it goes in a rule, not a nested &lt;code&gt;CLAUDE.md&lt;/code&gt;. If it's a procedure with steps, a script, or a template, it goes in a skill, and anything that shouldn't fire on its own gets &lt;code&gt;disable-model-invocation: true&lt;/code&gt;. If it's long enough that a human should read it too and Claude only needs it sometimes, it goes on disk.&lt;/p&gt;

&lt;p&gt;The two-tier version of this held up fine for a while. It just couldn't tell you where to put the thing that runs.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>productivity</category>
      <category>devtools</category>
    </item>
    <item>
      <title>There's no artisan schema:show, so I built one</title>
      <dc:creator>Alberto Arena</dc:creator>
      <pubDate>Fri, 24 Jul 2026 06:32:36 +0000</pubDate>
      <link>https://dev.to/alberto_arena/theres-no-artisan-schemashow-so-i-built-one-a2l</link>
      <guid>https://dev.to/alberto_arena/theres-no-artisan-schemashow-so-i-built-one-a2l</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwj0org21njz5fxc5vp6b.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwj0org21njz5fxc5vp6b.jpg" alt="Laravel Truss" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You join a project. Forty tables, half of them undocumented, and a &lt;code&gt;posts&lt;/code&gt; table that somehow relates to three other tables you've never heard of. Where do you start?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;php artisan db&lt;/code&gt; gets you an interactive shell, and from there you're back to whatever dialect your database speaks: &lt;code&gt;.schema&lt;/code&gt; on SQLite, &lt;code&gt;DESCRIBE&lt;/code&gt; on MySQL, &lt;code&gt;\d&lt;/code&gt; on Postgres. Laravel's own &lt;code&gt;db:show&lt;/code&gt; and &lt;code&gt;db:table&lt;/code&gt; commands are a step up, an overview of your tables or one table's columns, indexes, and foreign keys, printed straight to the terminal. But they're still one table at a time, in text. Nothing shows you the shape of the thing: which tables actually connect, and how, across the whole schema. So you end up either opening a DB client through an SSH tunnel, or drawing the diagram yourself on a whiteboard from memory, which is exactly as reliable as it sounds.&lt;/p&gt;

&lt;p&gt;And that's the easy case, when you have shell access at all. On staging or production, where you actually need this the most because that's where the real schema drifted from the migrations, you often don't have a shell. Some organisations block SSH outright for compliance, which is exactly the situation I &lt;a href="https://albertoarena.it/posts/beyond-the-bastion-aws-ssm-laravel-artisan/" rel="noopener noreferrer"&gt;wrote about replacing with AWS Systems Manager&lt;/a&gt; for running Artisan commands. Even with a Run Command session in hand, that gets you a shell again, not a diagram.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/albertoarena/laravel-truss" rel="noopener noreferrer"&gt;Truss&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;Truss is a live database structure viewer for Laravel. It scans your actual schema, not your migration files, and renders it as a scrollable, zoomable ER diagram right inside your app.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live ER diagram&lt;/strong&gt;, rendered with Mermaid, of every table and how it connects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus mode&lt;/strong&gt;: click a table to isolate it and its foreign-key neighbours, centred and highlighted, so you're not staring at forty tables to understand one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter by table name&lt;/strong&gt;, and toggle native column types against Laravel-style labels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map-style pan and zoom&lt;/strong&gt;, with auto-fit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure only, always.&lt;/strong&gt; Truss reads tables, columns, keys, and indexes. It never queries a single row, which is the whole point of being able to run it somewhere that matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-contained&lt;/strong&gt;: Mermaid and fonts are vendored and served from the package, so it works offline and under a strict CSP. No CDN, nothing phoning home.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cached and automatic&lt;/strong&gt;: the snapshot rebuilds after migrations, so the diagram never goes stale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export the whole diagram&lt;/strong&gt; as PNG or SVG, exactly as it's currently filtered and focused, so it drops straight into a design doc or a PR description instead of a screenshot. Per-table exports as JSON or CSV are also available from each table's menu.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install it, visit &lt;code&gt;/truss&lt;/code&gt;, and you get the diagram. No config, no separate service to run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require albertoarena/laravel-truss
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default it's only enabled locally. To use it on staging or production you explicitly enable it and gate access behind a &lt;code&gt;viewTruss&lt;/code&gt; authorization gate, so "safe to run in prod" doesn't mean "wide open in prod".&lt;/p&gt;

&lt;h2&gt;
  
  
  From the command line, too
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;db:show&lt;/code&gt; and &lt;code&gt;db:table&lt;/code&gt; get you close, but they're still one table at a time, in text. &lt;code&gt;php artisan truss:show&lt;/code&gt; prints that same cached, exclusion-filtered snapshot the diagram uses, as a table: one row per database table, with its column count and foreign-key count, the whole schema at once instead of one piece of it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan truss:show   &lt;span class="c"&gt;# the whole schema as a table, in your terminal&lt;/span&gt;
php artisan truss:open   &lt;span class="c"&gt;# or jump straight to the diagram&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;php artisan truss:open&lt;/code&gt; opens the dashboard in your default browser, honouring the route prefix and app URL. It prints the URL regardless, so it still works over a forwarded port on a headless host with no browser for it to open.&lt;/p&gt;

&lt;p&gt;Both commands live under a &lt;code&gt;truss:&lt;/code&gt; namespace rather than &lt;code&gt;schema:&lt;/code&gt;. A package squatting Laravel's own &lt;code&gt;schema:&lt;/code&gt; namespace is asking for a collision if the framework ever ships a &lt;code&gt;schema:show&lt;/code&gt; of its own, which is the joke in this post's title.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it without installing anything
&lt;/h2&gt;

&lt;p&gt;If you want to poke at it before adding it to a project, there's a &lt;a href="https://albertoarena.github.io/laravel-truss/demo/" rel="noopener noreferrer"&gt;live demo&lt;/a&gt; running against a fictional schema, right in your browser. Pan, zoom, filter, focus a table, no install required.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's held up
&lt;/h2&gt;

&lt;p&gt;I've dropped Truss into a few of my own Laravel projects since building it, including ones I hadn't touched in a while, and it's the fastest I've re-oriented myself in a schema I half-remembered. Clicking through foreign keys in focus mode beats reconstructing the relationships from migration file names, every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://albertoarena.github.io/laravel-truss" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; covers installation, the quick start, and authorization for non-local environments in more detail. It requires PHP 8.3+ and Laravel 12+.&lt;/p&gt;

&lt;p&gt;Feedback welcome, as always.&lt;/p&gt;

&lt;p&gt;Known dev.to gotcha from the plan notes: pasting long content into its textarea can silently drop a newline at a random spot and merge two lines (has happened to two code blocks before) — worth a scroll-through of both code fences before publishing.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>database</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Claude Code Routines: A Third Way Beyond /goal and /loop</title>
      <dc:creator>Alberto Arena</dc:creator>
      <pubDate>Mon, 20 Jul 2026 06:24:20 +0000</pubDate>
      <link>https://dev.to/alberto_arena/claude-code-routines-a-third-way-beyond-goal-and-loop-1h4m</link>
      <guid>https://dev.to/alberto_arena/claude-code-routines-a-third-way-beyond-goal-and-loop-1h4m</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs13i1q23v8d6cl2xstpc.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs13i1q23v8d6cl2xstpc.jpg" alt="Claude Code Routines" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few months ago, Claude Code shipped &lt;a href="https://albertoarena.it/posts/goal-command-claude-code/" rel="noopener noreferrer"&gt;&lt;code&gt;/goal&lt;/code&gt;&lt;/a&gt;, a way to stop typing "keep going" every turn and let Claude work until a condition was actually met. That solved a real problem: staying in the loop with an open session so you didn't have to babysit it turn by turn. It didn't solve a different problem, the one where you want Claude to do something and you're not even at your laptop.&lt;/p&gt;

&lt;p&gt;That gap just closed. Anthropic added routines: saved configurations that run on Anthropic's own cloud infrastructure, triggered by a schedule, an API call, or a GitHub event, with no open session and no machine required. It's currently in research preview, so behavior and limits may still shift, but the shape of the feature is clear enough to be worth understanding now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a routine actually is
&lt;/h2&gt;

&lt;p&gt;A routine bundles three things: a prompt, one or more GitHub repositories, and a set of connectors (Slack, Linear, and other MCP integrations). Package it once, attach a trigger, and it runs on its own from then on, cloning the repository fresh each time and executing as a full, unattended Claude Code session.&lt;/p&gt;

&lt;p&gt;There's no permission-mode picker and no approval prompts during a run. Whatever the environment and connectors allow, Claude can do without asking. That's the point (nobody's there to answer the prompt) but it also means the blast radius of a routine is defined entirely by what you scope it to, not by anything you can intervene on mid-run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating one
&lt;/h2&gt;

&lt;p&gt;The fastest way in is &lt;code&gt;/schedule&lt;/code&gt; (alias &lt;code&gt;/routines&lt;/code&gt;), run inside any CLI session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/schedule daily PR review at 9am
&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;/schedule in 2 weeks, open a cleanup PR that removes the feature flag
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude asks follow-up questions about the repository, the prompt, and the cadence, then saves the routine to your claude.ai account. &lt;code&gt;/schedule&lt;/code&gt; only sets up schedule triggers though: API and GitHub triggers need the web form at claude.ai/code/routines, where you can also review or edit anything the CLI created. &lt;code&gt;/schedule list&lt;/code&gt;, &lt;code&gt;/schedule update&lt;/code&gt;, and &lt;code&gt;/schedule run&lt;/code&gt; manage existing routines from the terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to trigger one
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Scheduled.&lt;/strong&gt; A recurring cadence (hourly, nightly, weekly) or a single run at a specific future time. The minimum interval is one hour, so this isn't a replacement for tight polling, it's for the kind of check that genuinely only needs to happen once a day or once a week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API.&lt;/strong&gt; Each routine gets its own endpoint and bearer token. POST to it and a new session starts, optionally carrying a &lt;code&gt;text&lt;/code&gt; field with run-specific context, an alert body, a failing log line, whatever the caller wants to hand off. That text arrives wrapped in a block that labels it as untrusted data, so a routine's prompt has to explicitly opt in to acting on it. It's a deliberate guard: anyone holding a leaked token can send text, but they can't use it to redirect the routine into doing something the saved prompt never asked for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub events.&lt;/strong&gt; Pull request and release events, with filters on author, title, branch, labels, draft state, and merge state. A routine can watch for &lt;code&gt;pull_request.opened&lt;/code&gt; on a specific base branch and run your team's own review checklist before a human ever looks at the diff.&lt;/p&gt;

&lt;p&gt;A single routine can combine more than one trigger. A PR-review routine might run nightly as a backstop, fire from a deploy script, and also react to every new pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routines vs /goal vs /loop
&lt;/h2&gt;

&lt;p&gt;The three now cover genuinely different problems, not three flavors of the same one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/loop&lt;/code&gt;&lt;/strong&gt; re-runs a prompt on an interval, inside a session you have open, on your own machine. No completion check, just repetition. Good for polling a build or babysitting a PR while you're around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/goal&lt;/code&gt;&lt;/strong&gt; keeps a session working, on your machine, until a separate evaluator model confirms a condition holds. Good for a bounded task you kicked off and want finished properly before you look again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routines&lt;/strong&gt; run on Anthropic's infrastructure, with no session and no machine required, triggered by time, an API call, or a GitHub event. Good for anything that should happen whether or not you're at your desk.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical filter: if the automation needs to survive you closing your laptop, it has to be a routine. Neither &lt;code&gt;/loop&lt;/code&gt; nor &lt;code&gt;/goal&lt;/code&gt; does, both stop the moment the session ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where routines shine
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Backlog maintenance.&lt;/strong&gt; A schedule trigger set to &lt;code&gt;0 22 * * *&lt;/code&gt; (nightly at 10pm) reads issues opened since the last run, applies labels, assigns owners by code area, and posts a summary to a &lt;code&gt;#triage&lt;/code&gt; Slack channel before the team's day starts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alert triage.&lt;/strong&gt; A monitoring tool posts to the routine's API endpoint when an error threshold trips. The routine investigates the payload, correlates it with recent commits, and opens a draft PR with a proposed fix, so on-call reviews a diff instead of starting from a blank terminal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bespoke code review.&lt;/strong&gt; A GitHub trigger fires on every &lt;code&gt;pull_request.opened&lt;/code&gt;, applies your team's own checklist, and leaves inline comments for the mechanical stuff so human reviewers can focus on design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-repo porting.&lt;/strong&gt; A GitHub trigger on merged PRs in one SDK repository ports the same change to a parallel SDK in another language, keeping two libraries in step without someone re-implementing each change by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it gets tricky
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Everything a routine does carries your identity.&lt;/strong&gt; Commits and pull requests use your GitHub account. Slack messages and Linear tickets use your linked accounts. There's no separate "bot" identity to fall back on, so a routine with too many connectors attached can do a surprising amount in your name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No approval gate means the environment is the only safety net.&lt;/strong&gt; Network access, environment variables, and which connectors are included are configured once, up front, and apply to every run after that. Scope them to exactly what the routine needs, not to whatever happens to already be connected to your account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Green doesn't mean the task succeeded.&lt;/strong&gt; A green status means the session started and exited without an infrastructure error, nothing more. Whether Claude actually did what the prompt asked is a separate question you still have to check by reading the run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Branch pushes are restricted by default.&lt;/strong&gt; Claude can only push to &lt;code&gt;claude/&lt;/code&gt;-prefixed branches unless you explicitly allow unrestricted pushes for a repository. It's a sensible default, but worth knowing before a routine's first PR shows up on an unexpected branch name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is a research preview.&lt;/strong&gt; The API surface ships under a dated beta header, and Anthropic has already said behavior, limits, and field shapes may change. Don't build anything load-bearing on top of it just yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runs share your regular usage quota, plus a separate daily cap.&lt;/strong&gt; Routine runs draw down the same subscription usage as an interactive session, on top of an account-wide daily limit on how many routine runs can start. Set up several nightly routines and it's easy to assume each gets its own budget. It doesn't, so check your remaining runs at claude.ai/code/routines before you find out the hard way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical advice
&lt;/h2&gt;

&lt;p&gt;Start with a schedule trigger before reaching for API or GitHub triggers. It's the easiest to reason about and the easiest to pause if something looks wrong.&lt;/p&gt;

&lt;p&gt;Strip the connector list down to what the routine actually touches. Every connector you leave attached is something Claude can use without asking, on every run, indefinitely.&lt;/p&gt;

&lt;p&gt;Treat routine output the same way you'd treat a CI pipeline: review the diff, don't trust the status light. A run that "completed successfully" and a run that "did the right thing" are not the same claim.&lt;/p&gt;

&lt;p&gt;If a routine acts on external input (an alert body, a webhook payload), write the prompt to reference that payload explicitly. Otherwise it just sits there as inert context and the routine won't act on it at all, which is the safe failure mode, but worth knowing in advance rather than discovering it during an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/goal&lt;/code&gt; solved the problem of staying in a session without re-prompting it. Routines solve the problem of not needing a session at all. Pick the one that matches the problem you actually have, and for the first time, none of them requires bending one tool to do a different job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://code.claude.com/docs/en/routines" rel="noopener noreferrer"&gt;Automate work with routines&lt;/a&gt;: official Anthropic documentation for routines, covering creation, triggers, connectors, and limits.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://code.claude.com/docs/en/scheduled-tasks" rel="noopener noreferrer"&gt;Run prompts on a schedule&lt;/a&gt;: official documentation for &lt;code&gt;/loop&lt;/code&gt; and session-scoped scheduled tasks, including the comparison against routines and Desktop scheduled tasks.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://code.claude.com/docs/en/goal" rel="noopener noreferrer"&gt;Keep Claude working toward a goal&lt;/a&gt;: official documentation for the &lt;code&gt;/goal&lt;/code&gt; command.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://albertoarena.it/posts/goal-command-claude-code/" rel="noopener noreferrer"&gt;Claude Code /goal vs /loop: Stop Typing, Keep Going&lt;/a&gt;: the earlier post this one builds on.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>productivity</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
