<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/vendor/feed/atom.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US">
                        <id>https://freek.dev/feed</id>
                                <link href="https://freek.dev/feed" rel="self"></link>
                                <title><![CDATA[freek.dev - all blogposts]]></title>
                    
                                <subtitle>All blogposts on freek.dev</subtitle>
                                                    <updated>2026-09-21T12:30:31+02:00</updated>
                        <entry>
            <title><![CDATA[Introducing: Dark mode for Flare]]></title>
            <link rel="alternate" href="https://freek.dev/3193-introducing-dark-mode-for-flare" />
            <id>https://freek.dev/3193</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Flare now has a dark mode, and this post walks through the design work needed to make it feel right and stay accessible. Nice details on semantic color tokens, fixing gradients and shadows, and using screenshot comparisons to keep the light theme intact.</p>


<a href='https://flareapp.io/blog/introducing-dark-mode-for-flare'>Read more</a>]]>
            </summary>
                                    <updated>2026-09-21T12:30:31+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[★ Detecting spam and auto-replies with Jev and the Laravel AI SDK]]></title>
            <link rel="alternate" href="https://freek.dev/3194-detecting-spam-and-auto-replies-with-jev-and-the-laravel-ai-sdk" />
            <id>https://freek.dev/3194</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Yesterday <a href="https://x.com/taylorotwell/status/2100700952923713641">Taylor announced</a> that Jev support landed in the 1.x branch of the Laravel AI SDK. We started using it that same day for spam detection in <a href="https://there-there.app">There There</a>. Let's take a look at what Jev is and how you can use it.</p>
<!--more-->
<h2 id="how-jev-is-different-from-an-llm">How Jev is different from an LLM</h2>
<p><a href="https://docs.typesafe.ai">Jev</a> is made by TypeSafe. An LLM generates text: you ask it something, it writes an answer, and if you want structured data back you have to ask for it and hope. Jev doesn't generate anything. You give it some state and a question, and it gives you back a number.</p>
<p>TypeSafe calls these System One models. They read natural language like an LLM does, but instead of writing a reply they pick between answers that you define up front. The probabilities are calibrated, which means they're trained against real outcomes, so across a batch of answers a 0.9 should be right about nine times out of ten.</p>
<p>In practice that means no prose to parse, and no prompt asking the model to please respond with valid JSON.</p>
<p>The Syntax folks made a video explaining it:</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/QbYBRjOaGOo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
<h2 id="noul-choice-and-score">Noul, Choice and Score</h2>
<p>You define what the answers can be using one of three question types.</p>
<p>A Noul is a yes or no question. The answer is a single number: the probability that the answer is yes. Here's how you ask one:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Laravel\Ai\Classification</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Laravel\Ai\Classification\Boolean</span>;

<span class="hl-variable">$result</span> = <span class="hl-type">Classification</span>::<span class="hl-property">of</span>(<span class="hl-value">'I have asked three times now. Can I please talk to a real person?'</span>)
    -&gt;<span class="hl-property">question</span>(<span class="hl-value">'urgent'</span>, <span class="hl-keyword">new</span> <span class="hl-type">Boolean</span>(<span class="hl-value">'Does this request need an immediate response?'</span>))
    -&gt;<span class="hl-property">classify</span>();

<span class="hl-variable">$result</span>[<span class="hl-value">'urgent'</span>]-&gt;<span class="hl-property">probability</span>;            <span class="hl-comment">// 0.94</span>
<span class="hl-variable">$result</span>[<span class="hl-value">'urgent'</span>]-&gt;<span class="hl-property">isTrue</span>(<span class="hl-property">threshold</span>: 0.8); <span class="hl-comment">// true</span>
</pre>
<p>Notice that you get back <code>0.94</code> instead of <code>true</code>. You decide where the cutoff is, which means you can set a different one per question.</p>
<p>A Choice picks one option out of a set that you name. Here's an example:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Laravel\Ai\Classification\Choice</span>;

<span class="hl-variable">$result</span> = <span class="hl-type">Classification</span>::<span class="hl-property">of</span>(<span class="hl-value">'My card was charged twice for order A-104. Please refund the duplicate.'</span>)
    -&gt;<span class="hl-property">question</span>(<span class="hl-value">'department'</span>, <span class="hl-keyword">new</span> <span class="hl-type">Choice</span>(<span class="hl-value">'Which team should handle this request?'</span>, [
        <span class="hl-value">'billing'</span> =&gt; <span class="hl-value">'Payments, invoices, and refunds'</span>,
        <span class="hl-value">'technical'</span> =&gt; <span class="hl-value">'Bugs, outages, and integrations'</span>,
        <span class="hl-value">'sales'</span> =&gt; <span class="hl-value">'Pricing, plans, and upgrades'</span>,
    ]))
    -&gt;<span class="hl-property">classify</span>();

<span class="hl-variable">$result</span>[<span class="hl-value">'department'</span>]-&gt;<span class="hl-property">choice</span>;                   <span class="hl-comment">// 'billing'</span>
<span class="hl-variable">$result</span>[<span class="hl-value">'department'</span>]-&gt;<span class="hl-property">probabilityOf</span>(<span class="hl-value">'billing'</span>); <span class="hl-comment">// 0.87</span>
<span class="hl-variable">$result</span>[<span class="hl-value">'department'</span>]-&gt;<span class="hl-property">confidence</span>;               <span class="hl-comment">// 0.82</span>
</pre>
<p>Next to the option it picked, you also get a probability for every option, and a confidence score that tells you how concentrated those probabilities were.</p>
<p>A Score rates something against levels that you describe yourself. You could ask how frustrated a customer is, where 0 is calm, 1 is frustrated and 2 is very angry. The answer can land between two levels, so a 1.4 is a perfectly good answer.</p>
<p>The state doesn't have to be a string. When a decision depends on more than one thing, you can pass an array so every part has a name:</p>
<pre data-lang="php" class="notranslate"><span class="hl-type">Classification</span>::<span class="hl-property">of</span>([
    <span class="hl-value">'subject'</span> =&gt; <span class="hl-value">'Duplicate charge'</span>,
    <span class="hl-value">'message'</span> =&gt; <span class="hl-value">'My card was charged twice for order A-104.'</span>,
    <span class="hl-value">'order'</span> =&gt; [<span class="hl-value">'id'</span> =&gt; <span class="hl-value">'A-104'</span>, <span class="hl-value">'charges'</span> =&gt; [49, 49]],
    <span class="hl-value">'refund_policy'</span> =&gt; <span class="hl-value">'Duplicate charges are eligible for a refund.'</span>,
])-&gt;<span class="hl-property">question</span>(<span class="hl-value">'refund_due'</span>, <span class="hl-keyword">new</span> <span class="hl-type">Boolean</span>(<span class="hl-value">'The policy entitles this customer to a refund.'</span>))
    -&gt;<span class="hl-property">classify</span>();
</pre>
<p>That's still one state, even though it holds a message, an order and a policy.</p>
<p>You configure Jev like any other provider in <code>config/ai.php</code>, with a <code>TYPESAFE_API_KEY</code> in your env file.</p>
<h2 id="the-problem-we-wanted-to-solve">The problem we wanted to solve</h2>
<p>You might have noticed that we <a href="https://x.com/freekmurze/status/2100549614248308805">launched There There</a> yesterday as well, our new helpdesk. A lot of the mail that arrives in a helpdesk isn't from a customer. Out-of-office replies, bounces, subscription confirmations, DMARC reports. You don't want those in your inbox, and you don't want to pay an LLM to write a title and a summary for each one.</p>
<p>Some of that mail says what it is in the headers. <code>Auto-Submitted</code>, an empty return path, a sender called mailer-daemon. Checking those costs nothing, so we do that first.</p>
<p>Plenty of mail servers don't set those headers. For those we had a list of subject prefixes: <code>Automatische Antwort</code>, <code>Réponse automatique</code>, <code>Out of office</code>, in fifteen languages. Nine more for bounces. On top of that, rules so that a customer asking a question about auto-replies didn't get treated as one.</p>
<p>Every time we found mail that the list missed, we added another string to it.</p>
<h2 id="replacing-the-list">Replacing the list</h2>
<p>The header checks still run first. Everything they can't answer goes to Jev.</p>
<p>We describe each question once, as a case on an enum that also carries its own threshold. That way adding a fourth question is a single case instead of an edit in four files.</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">enum</span> <span class="hl-type">InboundJudgement</span>: <span class="hl-type">string</span>
{
    <span class="hl-keyword">case</span> <span class="hl-property">IsAutoResponse</span> = <span class="hl-value">'is_auto_response'</span>;
    <span class="hl-keyword">case</span> <span class="hl-property">IsBounce</span> = <span class="hl-value">'is_bounce'</span>;
    <span class="hl-keyword">case</span> <span class="hl-property">IsSpam</span> = <span class="hl-value">'is_spam'</span>;

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">question</span>(): <span class="hl-type">Boolean</span>
    {
        <span class="hl-keyword">return</span> <span class="hl-keyword">match</span> (<span class="hl-variable">$this</span>) {
            <span class="hl-type">self</span>::<span class="hl-property">IsAutoResponse</span> =&gt; <span class="hl-keyword">new</span> <span class="hl-type">Boolean</span>(
                <span class="hl-value">'A system sent this mail on its own, rather than a person choosing to write to us.'</span>,
                [
                    <span class="hl-value">'true'</span> =&gt; 'Sent on a trigger with no human involved at send <span class="hl-property">time</span>: out-of-office
                        notices, delivery reports, ticket acknowledgements, subscription
                        confirmations, digests <span class="hl-keyword">and</span> alerts. Wording composed in advance still counts',
                    <span class="hl-value">'false'</span> =&gt; '<span class="hl-property">A</span> person sat down <span class="hl-keyword">and</span> sent this. Still <span class="hl-keyword">false</span> when a contact form <span class="hl-keyword">or</span>
                        chat widget wrapped their words in a template <span class="hl-keyword">and</span> added lines such <span class="hl-keyword">as</span> Name,
                        <span class="hl-property">E</span>-mail <span class="hl-keyword">or</span> Subject',
                ],
            ),
            <span class="hl-type">self</span>::<span class="hl-property">IsSpam</span> =&gt; <span class="hl-keyword">new</span> <span class="hl-type">Boolean</span>(
                'This mail is unsolicited bulk mail, a scam, <span class="hl-keyword">or</span> phishing rather than a genuine
                    message from a customer.',
                [
                    <span class="hl-value">'true'</span> =&gt; 'Cold sales outreach, marketing blasts, scams, phishing, <span class="hl-keyword">or</span> anything
                        the recipient never asked for',
                    <span class="hl-value">'false'</span> =&gt; '<span class="hl-property">A</span> real person writing about the product, their account, <span class="hl-keyword">or</span> their own
                        support request, however brief <span class="hl-keyword">or</span> badly written',
                ],
            ),
            <span class="hl-comment">// ...</span>
        };
    }

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">threshold</span>(): <span class="hl-type">float</span>
    {
        <span class="hl-keyword">return</span> <span class="hl-keyword">match</span> (<span class="hl-variable">$this</span>) {
            <span class="hl-type">self</span>::<span class="hl-property">IsAutoResponse</span> =&gt; 0.75,
            <span class="hl-type">self</span>::<span class="hl-property">IsBounce</span>, <span class="hl-type">self</span>::<span class="hl-property">IsSpam</span> =&gt; 0.9,
        };
    }
}
</pre>
<p>Those <code>true</code> and <code>false</code> descriptions are optional, but I'd recommend writing them. They do more work than the question above them.</p>
<p>All the questions the headers couldn't answer go out in one request. Jev reads the state once and answers them in parallel, and you only pay for input tokens, so asking three questions costs the same as asking one. Here's the action that does it:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">execute</span>(<span class="hl-injection"><span class="hl-type">Message</span> $message, <span class="hl-type">Ticket</span> $ticket, <span class="hl-type">Workspace</span> $workspace</span>): <span class="hl-type">void</span>
{
    <span class="hl-variable">$judgements</span> = <span class="hl-property">array_filter</span>(
        <span class="hl-type">InboundJudgement</span>::<span class="hl-property">cases</span>(),
        <span class="hl-keyword">fn</span> (<span class="hl-injection"><span class="hl-type">InboundJudgement</span> $judgement</span>) =&gt; ! <span class="hl-variable">$judgement</span>-&gt;<span class="hl-property">settledByHeaders</span>(<span class="hl-variable">$message</span>),
    );

    <span class="hl-keyword">if</span> (<span class="hl-variable">$judgements</span> === []) {
        <span class="hl-keyword">return</span>;
    }

    <span class="hl-keyword">try</span> {
        <span class="hl-variable">$response</span> = <span class="hl-type">Classification</span>::<span class="hl-property">of</span>([
            <span class="hl-value">'subject'</span> =&gt; <span class="hl-variable">$ticket</span>-&gt;<span class="hl-property">subject</span>,
            <span class="hl-value">'from_name'</span> =&gt; <span class="hl-variable">$message</span>-&gt;<span class="hl-property">author_name</span>,
            <span class="hl-value">'from_email'</span> =&gt; <span class="hl-variable">$message</span>-&gt;<span class="hl-property">author_email</span> ?? <span class="hl-variable">$ticket</span>-&gt;<span class="hl-property">contact</span>?-&gt;<span class="hl-property">email</span>,
            <span class="hl-value">'message'</span> =&gt; <span class="hl-type">Str</span>::<span class="hl-property">limit</span>(<span class="hl-variable">$message</span>-&gt;<span class="hl-property">body_text</span>, 10_000),
        ])
            -&gt;<span class="hl-property">questions</span>(<span class="hl-variable">$this</span>-&gt;<span class="hl-property">questionsFor</span>(<span class="hl-variable">$judgements</span>))
            -&gt;<span class="hl-property">timeout</span>(10)
            -&gt;<span class="hl-property">classify</span>();

        <span class="hl-variable">$verdicts</span> = <span class="hl-variable">$this</span>-&gt;<span class="hl-property">verdicts</span>(<span class="hl-variable">$judgements</span>, <span class="hl-variable">$response</span>);
    } <span class="hl-keyword">catch</span> (<span class="hl-type">Throwable</span> <span class="hl-variable">$exception</span>) {
        <span class="hl-type">Log</span>::<span class="hl-property">warning</span>(<span class="hl-value">'Could not classify an inbound message.'</span>, [
            <span class="hl-value">'message_id'</span> =&gt; <span class="hl-variable">$message</span>-&gt;<span class="hl-property">id</span>,
            <span class="hl-value">'error'</span> =&gt; <span class="hl-variable">$exception</span>-&gt;<span class="hl-property">getMessage</span>(),
        ]);

        <span class="hl-keyword">return</span>;
    }

    <span class="hl-variable">$message</span>-&gt;<span class="hl-property">updateQuietly</span>([...<span class="hl-variable">$verdicts</span>, <span class="hl-value">'classification'</span> =&gt; <span class="hl-variable">$response</span>-&gt;<span class="hl-property">answers</span>]);
}
</pre>
<p>There are two things in there I'd suggest copying. The whole call sits in a try block that logs the problem and moves on, because a classification is a nice to have and it shouldn't be able to break the mail pipeline it's helping. And we truncate the message, because an inbound mail can be megabytes long and nothing past the first part of it changes what the mail is.</p>
<p>Turning the answers into booleans is where each question's own threshold is applied. We store the raw probabilities next to them, so we can change a threshold later and see what it would have done:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">private</span> <span class="hl-keyword">function</span> <span class="hl-property">verdicts</span>(<span class="hl-injection"><span class="hl-type">array</span> $judgements, <span class="hl-type">ClassificationResponse</span> $response</span>): <span class="hl-type">array</span>
{
    <span class="hl-variable">$verdicts</span> = [];

    <span class="hl-keyword">foreach</span> (<span class="hl-variable">$judgements</span> <span class="hl-keyword">as</span> <span class="hl-variable">$judgement</span>) {
        <span class="hl-variable">$answer</span> = <span class="hl-variable">$response</span>-&gt;<span class="hl-property">answer</span>(<span class="hl-variable">$judgement</span>-&gt;<span class="hl-property">value</span>);

        <span class="hl-variable">$verdicts</span>[<span class="hl-variable">$judgement</span>-&gt;<span class="hl-property">value</span>] = <span class="hl-variable">$answer</span>-&gt;<span class="hl-property">isTrue</span>(<span class="hl-variable">$judgement</span>-&gt;<span class="hl-property">threshold</span>());
    }

    <span class="hl-keyword">return</span> <span class="hl-variable">$verdicts</span>;
}
</pre>
<p>Those verdicts are stored on the message. When a customer builds a workflow in There There with an &quot;Is spam&quot; condition, checking that condition reads a single column and doesn't call Jev at all.</p>
<h2 id="in-closing">In closing</h2>
<p>I like that Jev does one small thing. It gives you a number and leaves the rest of the decisions in your own code, where you can read them and write tests for them.</p>
<p>It's also fast and cheap enough that you don't really have to think about it. Classifying a mail with three questions at once takes 639ms, and we get around 48 per second when we run them in parallel. We didn't spend any time tuning that, so I'm sure you could get more out of it, but for what we're doing it's fast enough. Jev costs $0.042 per million input tokens and output tokens are free, which for us comes down to four hundredths of a cent per mail, or about 36 cents a month.</p>
<p>We have a list of other places where we want to use this in There There, and in our other products. Expect more Jev powered features soon.</p>
<p>If you want to read more, there are the <a href="https://docs.typesafe.ai">TypeSafe docs</a> and the <a href="https://github.com/laravel/ai">Laravel AI SDK</a>. And if you'd like to see the spam detection at work, you can try <a href="https://there-there.app">There There</a>.</p>
]]>
            </summary>
                                    <updated>2026-09-18T17:39:46+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[What's New in PHP 8.6]]></title>
            <link rel="alternate" href="https://freek.dev/3192-whats-new-in-php-86" />
            <id>https://freek.dev/3192</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A concise overview of what's coming in PHP 8.6, including partial function application, clamp(), the new Duration class, and several smaller language and standard library improvements.</p>


<a href='https://laravel-news.com/php-8-6'>Read more</a>]]>
            </summary>
                                    <updated>2026-09-16T14:30:30+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Logarithmic auto-scaling for Laravel Horizon]]></title>
            <link rel="alternate" href="https://freek.dev/3191-logarithmic-auto-scaling-for-laravel-horizon" />
            <id>https://freek.dev/3191</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A thoughtful write-up on adding logarithmic auto-scaling to Laravel Horizon. It shows how log-based weighting keeps huge queue spikes from starving smaller realtime queues while still giving large backlogs enough workers.</p>


<a href='https://gummibeer.dev/blog/2026/logarithmic-auto-scaling-laravel-horizon'>Read more</a>]]>
            </summary>
                                    <updated>2026-09-12T14:30:30+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Eloquent Performance and Database Design: Evidence Before Eager Loading]]></title>
            <link rel="alternate" href="https://freek.dev/3190-eloquent-performance-and-database-design-evidence-before-eager-loading" />
            <id>https://freek.dev/3190</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A deep dive into Eloquent performance, from detecting N+1 queries to choosing aggregates, indexes, query plans, pagination, chunking, and transaction boundaries for a growing team dashboard.</p>


<a href='https://wendelladriel.com/blog/eloquent-performance-and-database-design-evidence-before-eager-loading'>Read more</a>]]>
            </summary>
                                    <updated>2026-09-11T14:30:27+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[A Series of Unfortunate Jobs]]></title>
            <link rel="alternate" href="https://freek.dev/3189-a-series-of-unfortunate-jobs" />
            <id>https://freek.dev/3189</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Laravel queues are great out of the box, until they bite. Over the years I’ve collected a few gotchas the hard way. Let me save you the headache.</p>


<a href='https://oussama-mater.tech/laravel-queue-gotchas'>Read more</a>]]>
            </summary>
                                    <updated>2026-09-10T14:06:27+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Run AI in the Browser: A Practical Guide to Transformers.js]]></title>
            <link rel="alternate" href="https://freek.dev/3188-run-ai-in-the-browser-a-practical-guide-to-transformersjs" />
            <id>https://freek.dev/3188</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Transformers.js lets you run AI models directly in the browser without a backend, API keys, or an internet connection after the model is cached. The article explores how it works, which models are available, and the trade-offs of client-side AI compared to traditional AI providers.</p>


<a href='https://tighten.com/insights/run-ai-in-the-browser-a-practical-guide-to-transformers-js/'>Read more</a>]]>
            </summary>
                                    <updated>2026-09-09T16:50:26+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Using AI as a deterministic translation tool in Laravel]]></title>
            <link rel="alternate" href="https://freek.dev/3186-using-ai-as-a-deterministic-translation-tool-in-laravel" />
            <id>https://freek.dev/3186</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Split a task into reasoning and execution to get deterministic behaviour out of a non-deterministic model.</p>


<a href='https://koomai.net/posts/using-ai-as-a-deterministic-translation-tool-in-laravel/'>Read more</a>]]>
            </summary>
                                    <updated>2026-09-07T14:53:24+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Digital Sovereignty Is Written in PHP]]></title>
            <link rel="alternate" href="https://freek.dev/3185-digital-sovereignty-is-written-in-php" />
            <id>https://freek.dev/3185</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>An interesting article on how PHP is used by the government.</p>


<a href='https://thephp.foundation/blog/2026/09/02/digital-sovereignty-is-written-in-php/'>Read more</a>]]>
            </summary>
                                    <updated>2026-09-04T14:30:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Build Your Own AI-Powered Slack Bot with the Laravel AI SDK]]></title>
            <link rel="alternate" href="https://freek.dev/3184-build-your-own-ai-powered-slack-bot-with-the-laravel-ai-sdk" />
            <id>https://freek.dev/3184</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>In this article, we’ll build an AI-powered Slack bot with the Laravel AI SDK. Unlike tools such as Claude Tag, it can switch AI providers, answer questions, execute tasks, and retrieve information from a knowledge base you control.</p>


<a href='https://tighten.com/insights/build-your-own-ai-powered-slack-bot-with-the-laravel-ai-sdk/'>Read more</a>]]>
            </summary>
                                    <updated>2026-08-31T14:59:25+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Is AI going to steal my job as a design engineer?]]></title>
            <link rel="alternate" href="https://freek.dev/3183-is-ai-going-to-steal-my-job-as-a-design-engineer" />
            <id>https://freek.dev/3183</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Tom Wilson reflects on what AI is changing for design engineers, and where human judgment still matters. He argues that agents can take over more of the scaffolding work, while product taste, restraint, and polished visual craft remain the real edge.</p>


<a href='https://twilson.net/writing/ai-design-engineer'>Read more</a>]]>
            </summary>
                                    <updated>2026-08-25T14:30:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Modular Monoliths: Creating Real Boundaries Before Reaching for Microservices]]></title>
            <link rel="alternate" href="https://freek.dev/3182-modular-monoliths-creating-real-boundaries-before-reaching-for-microservices" />
            <id>https://freek.dev/3182</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A deep dive into modular monoliths, from module APIs and database ownership to cross-module communication, architecture tests, incremental migration, and the signals that justify microservices.</p>


<a href='https://wendelladriel.com/blog/modular-monoliths-creating-real-boundaries-before-reaching-for-microservices'>Read more</a>]]>
            </summary>
                                    <updated>2026-08-20T15:48:25+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Immutability in PHP Beyond readonly]]></title>
            <link rel="alternate" href="https://freek.dev/3181-immutability-in-php-beyond-readonly" />
            <id>https://freek.dev/3181</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A deep dive into immutability in PHP, from readonly properties and interior mutability to immutable value objects, collections, dates, cloning, boundaries, and testing.</p>


<a href='https://wendelladriel.com/blog/immutability-in-php-beyond-readonly'>Read more</a>]]>
            </summary>
                                    <updated>2026-08-17T14:12:30+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Flare now uses OAuth for MCP, the CLI, and API]]></title>
            <link rel="alternate" href="https://freek.dev/3180-flare-now-uses-oauth-for-mcp-the-cli-and-api" />
            <id>https://freek.dev/3180</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>We explain how Flare now uses OAuth to connect MCP clients, the CLI, and the API with scoped permissions instead of broad long-lived tokens. The new flow makes setup faster, adds read-only and project-level access control, and gives you one place to manage every connection.</p>


<a href='https://flareapp.io/blog/flare-now-uses-oauth-for-mcp-clients-the-cli-and-the-api'>Read more</a>]]>
            </summary>
                                    <updated>2026-08-15T12:30:29+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Can we make default Tailwind a more accessible choice?]]></title>
            <link rel="alternate" href="https://freek.dev/3179-can-we-make-default-tailwind-a-more-accessible-choice" />
            <id>https://freek.dev/3179</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A sharp look at Tailwind's default rem-based breakpoints, and the accessibility tradeoff they make. It explains how browser font-size preferences can shift layouts, why that can help some users, and why px breakpoints are still a defensible choice.</p>


<a href='https://spatie.be/blog/can-we-make-default-tailwind-a-more-accessible-choice'>Read more</a>]]>
            </summary>
                                    <updated>2026-08-14T12:30:33+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Why we removed AI solutions from Flare]]></title>
            <link rel="alternate" href="https://freek.dev/3178-why-we-removed-ai-solutions-from-flare" />
            <id>https://freek.dev/3178</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>We explain why Flare's old AI-generated solutions no longer fit the way developers debug with modern coding agents. The replacement is a better workflow built around the MCP server, CLI, Copy for AI prompts, and agent-friendly docs.</p>


<a href='https://flareapp.io/blog/why-we-removed-ai-solutions-from-flare'>Read more</a>]]>
            </summary>
                                    <updated>2026-08-13T12:30:30+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[`exit()` may silently break your parallel tests]]></title>
            <link rel="alternate" href="https://freek.dev/3177-exit-may-silently-break-your-parallel-tests" />
            <id>https://freek.dev/3177</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Michael explains why exit() can make parallel test workers crash without useful diagnostics, and why verbose flags do not help when the process dies outside PHPUnit's control. The fix is simple: throw an exception instead, so the failure is reported normally with a stack trace.</p>


<a href='https://dyrynda.com.au/blog/exit-may-silently-break-your-parallel-tests'>Read more</a>]]>
            </summary>
                                    <updated>2026-08-12T12:30:31+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Amped up]]></title>
            <link rel="alternate" href="https://freek.dev/3176-amped-up" />
            <id>https://freek.dev/3176</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Sebastian explains why Amp became his default coding harness, and why user experience now matters more to him than model churn. He also shares the AGENTS.md tweaks that reduced overzealous verification during day-to-day coding.</p>


<a href='https://sebastiandedeyne.com/amped-up/'>Read more</a>]]>
            </summary>
                                    <updated>2026-08-11T12:30:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[sbx: Sandboxed Claude, complete with PHP and tools]]></title>
            <link rel="alternate" href="https://freek.dev/3175-sbx-sandboxed-claude-complete-with-php-and-tools" />
            <id>https://freek.dev/3175</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A practical walkthrough for running Claude Code inside Docker's new sbx sandbox, with a custom PHP setup and extra tools for open source work. Benjamin also explains why sandboxing AI coding agents is the sensible default.</p>


<a href='https://www.beberlei.de/post/sbx-sandboxed-claude-complete-with-php-and-tools'>Read more</a>]]>
            </summary>
                                    <updated>2026-08-10T12:30:04+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[What's new in PHP 8.6]]></title>
            <link rel="alternate" href="https://freek.dev/3174-whats-new-in-php-86" />
            <id>https://freek.dev/3174</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Brent surveys the most notable additions coming in PHP 8.6, including partial function application, the new polling API, readonly property defaults, and several smaller improvements and deprecations.</p>


<a href='https://stitcher.io/blog/new-in-php-86'>Read more</a>]]>
            </summary>
                                    <updated>2026-08-05T14:14:26+02:00</updated>
        </entry>
    </feed>
