<?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-05-02T14:30:29+02:00</updated>
                        <entry>
            <title><![CDATA[On pricing]]></title>
            <link rel="alternate" href="https://freek.dev/3095-on-pricing" />
            <id>https://freek.dev/3095</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Seth Godin shares a compact set of pricing truths about value, story, and perception. It is especially good on why the right answer to &quot;that's too expensive&quot; is often a better story, not a lower price.</p>


<a href='https://seths.blog/2026/04/on-pricing/'>Read more</a>]]>
            </summary>
                                    <updated>2026-05-02T14:30:29+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[RBAC in Laravel: A Practical Deep Dive]]></title>
            <link rel="alternate" href="https://freek.dev/3101-rbac-in-laravel-a-practical-deep-dive" />
            <id>https://freek.dev/3101</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 Role-Based Access Control, from the theory behind roles and permissions to a practical, team-aware Laravel implementation without external packages.</p>


<a href='https://wendelladriel.com/blog/rbac-in-laravel-a-practical-deep-dive'>Read more</a>]]>
            </summary>
                                    <updated>2026-05-01T14:40:29+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Laravel Route Binding Behind the Curtains]]></title>
            <link rel="alternate" href="https://freek.dev/3093-laravel-route-binding-behind-the-curtains" />
            <id>https://freek.dev/3093</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 how Laravel transforms raw route segments into models, scoped children, enums, and custom bound values before your controller runs.</p>


<a href='https://wendelladriel.com/blog/laravel-route-binding-behind-the-curtains'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-30T14:40:31+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[★ Announcing laravel-sluggable v4 with self-healing URLs]]></title>
            <link rel="alternate" href="https://freek.dev/3112-announcing-laravel-sluggable-v4-with-self-healing-urls" />
            <id>https://freek.dev/3112</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>The <a href="https://github.com/spatie/laravel-sluggable"><code>spatie/laravel-sluggable</code></a> package has been around for close to a decade. A slug is the readable part of a URL that identifies a record, like <code>announcing-laravel-sluggable-v4-with-self-healing-urls</code> in this post's URL. The package generates one for any Eloquent model when you save it, derived from a title or another text field, and most of the time you don't have to think about it.</p>
<p>We just released v4, which adds a few things worth talking about. Let me walk you through them.</p>
<!--more-->
<h2 id="generating-slugs-with-an-attribute">Generating slugs with an attribute</h2>
<p>For most models, slug generation is mechanical. Pick a source field, pick a destination field, done. In v4, you can express that with an attribute on the model.</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Spatie\Sluggable\Attributes\Sluggable</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Illuminate\Database\Eloquent\Model</span>;

<span class="hl-injection"><span class="hl-attribute">#[<span class="hl-type">Sluggable</span>(<span class="hl-property">from</span>: <span class="hl-value">'title'</span>, <span class="hl-property">to</span>: <span class="hl-value">'slug'</span>)]</span></span>
<span class="hl-keyword">class</span> <span class="hl-type">Post</span> <span class="hl-keyword">extends</span> <span class="hl-type">Model</span>
{
}
</pre>
<p>That's it. No trait, no <code>getSlugOptions()</code> method. Save a <code>Post</code> and the package fills in the slug:</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$post</span> = <span class="hl-type">Post</span>::<span class="hl-property">create</span>([<span class="hl-value">'title'</span> =&gt; <span class="hl-value">'ActiveRecord is awesome'</span>]);

<span class="hl-variable">$post</span>-&gt;<span class="hl-property">slug</span>; <span class="hl-comment">// &quot;activerecord-is-awesome&quot;</span>
</pre>
<h3 id="self-healing-urls">Self-healing URLs</h3>
<p>Picture this. You publish a new post titled &quot;Anouncing v4 of laravel-sluggable&quot;. The slug becomes <code>anouncing-v4-of-laravel-sluggable</code>, you share the link, a few people bookmark it. Five minutes later you notice the missing <code>n</code> and fix the title. The slug regenerates to <code>announcing-v4-of-laravel-sluggable</code>, and now everyone who clicked the first version hits a 404.</p>
<p>The usual answer is a redirects table: store the old slug, forward it to the new one. It works, but every rename adds a row, and after a while it's another moving piece to maintain.</p>
<p>Self-healing URLs solve this differently. The route key combines the slug with the primary key, so the slug is decorative and the id is what actually identifies the record.</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Spatie\Sluggable\Attributes\Sluggable</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Spatie\Sluggable\HasSlug</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Illuminate\Database\Eloquent\Model</span>;

<span class="hl-injection"><span class="hl-attribute">#[<span class="hl-type">Sluggable</span>(<span class="hl-property">from</span>: <span class="hl-value">'title'</span>, <span class="hl-property">to</span>: <span class="hl-value">'slug'</span>, <span class="hl-property">selfHealing</span>: <span class="hl-keyword">true</span>)]</span></span>
<span class="hl-keyword">class</span> <span class="hl-type">Post</span> <span class="hl-keyword">extends</span> <span class="hl-type">Model</span>
{
    <span class="hl-keyword">use</span> <span class="hl-type">HasSlug</span>;
}
</pre>
<p>If your post has slug <code>hello-world</code> and id <code>5</code>, the URL becomes <code>/posts/hello-world-5</code>. Rename the post to <code>Greetings, World</code> and the canonical URL becomes <code>/posts/greetings-world-5</code>. Old links keep working because the package can still find the record by id, and it sends a permanent redirect to the new canonical URL.</p>
<p>The redirect status code is <code>308 Permanent Redirect</code>. In v3 it was <code>301</code>. The reason for the change is that <code>301</code> is allowed to silently downgrade <code>PUT</code>, <code>PATCH</code>, and <code>DELETE</code> to <code>GET</code> when followed, which gets you a <code>405 Method Not Allowed</code> if the new URL doesn't accept <code>GET</code>. <code>308</code> keeps the method intact.</p>
<h3 id="overridable-actions">Overridable actions</h3>
<p>Slug generation and the self-healing URL format are now expressed as three actions: one for generating the slug, one for building the self-healing route key, and one for parsing it back out. All three can be swapped through <code>config/sluggable.php</code>. The example below customizes the two self-healing actions; the slug generator stays on its default behavior.</p>
<p>The use case I had in mind was self-healing URLs that put the id first. Say you want <code>/posts/5-hello-world</code> instead of <code>/posts/hello-world-5</code>. Extend the default builder action and put the identifier in front:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">namespace</span> <span class="hl-type">App\Sluggable</span>;

<span class="hl-keyword">use</span> <span class="hl-type">Spatie\Sluggable\Actions\BuildSelfHealingRouteKeyAction</span>;

<span class="hl-keyword">class</span> <span class="hl-type">IdFirstBuildAction</span> <span class="hl-keyword">extends</span> <span class="hl-type">BuildSelfHealingRouteKeyAction</span>
{
    <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">string</span> $slug, <span class="hl-type">int|string</span> $identifier, <span class="hl-type">string</span> $separator</span>): <span class="hl-type">string</span>
    {
        <span class="hl-keyword">if</span> (<span class="hl-variable">$slug</span> === <span class="hl-value">''</span>) {
            <span class="hl-keyword">return</span> (<span class="hl-type">string</span>) <span class="hl-variable">$identifier</span>;
        }

        <span class="hl-keyword">return</span> <span class="hl-value">&quot;{$identifier}{$separator}{$slug}&quot;</span>;
    }
}
</pre>
<p>The extractor needs the inverse logic. The default action looks for the last separator, because slugs can contain the separator themselves, so swap it for one that reads the identifier from the front:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">namespace</span> <span class="hl-type">App\Sluggable</span>;

<span class="hl-keyword">use</span> <span class="hl-type">Illuminate\Support\Str</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Spatie\Sluggable\Actions\ExtractIdentifierFromSelfHealingRouteKeyAction</span>;

<span class="hl-keyword">class</span> <span class="hl-type">IdFirstExtractAction</span> <span class="hl-keyword">extends</span> <span class="hl-type">ExtractIdentifierFromSelfHealingRouteKeyAction</span>
{
    <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">string</span> $value, <span class="hl-type">string</span> $separator</span>): <span class="hl-type">array</span>
    {
        <span class="hl-variable">$identifier</span> = <span class="hl-type">Str</span>::<span class="hl-property">before</span>(<span class="hl-variable">$value</span>, <span class="hl-variable">$separator</span>);

        <span class="hl-keyword">if</span> (<span class="hl-variable">$identifier</span> === <span class="hl-variable">$value</span> <span class="hl-operator">||</span> ! <span class="hl-property">ctype_digit</span>(<span class="hl-variable">$identifier</span>)) {
            <span class="hl-keyword">return</span> [<span class="hl-value">'slug'</span> =&gt; <span class="hl-variable">$value</span>, <span class="hl-value">'identifier'</span> =&gt; null];
        }

        <span class="hl-keyword">return</span> [
            <span class="hl-value">'slug'</span> =&gt; <span class="hl-type">Str</span>::<span class="hl-property">after</span>(<span class="hl-variable">$value</span>, <span class="hl-variable">$separator</span>),
            <span class="hl-value">'identifier'</span> =&gt; <span class="hl-variable">$identifier</span>,
        ];
    }
}
</pre>
<p><code>Str::before</code> returns the original string when the separator isn't present, which is how the no-separator case is detected. The <code>ctype_digit</code> check rejects values where the prefix isn't numeric, so models with non-numeric primary keys like ULIDs need to swap that for a regex.</p>
<p>Wire both into the config and you're done:</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">// config/sluggable.php</span>
<span class="hl-value">'build_self_healing_route_key'</span> =&gt; <span class="hl-type">App\Sluggable\IdFirstBuildAction</span>::<span class="hl-keyword">class</span>,
<span class="hl-value">'extract_identifier_from_self_healing_route_key'</span> =&gt; <span class="hl-type">App\Sluggable\IdFirstExtractAction</span>::<span class="hl-keyword">class</span>,
</pre>
<p>The full reference, including a simpler uppercase variant and a custom slug generator, lives in the <a href="https://spatie.be/docs/laravel-sluggable/v4/advanced-usage/overriding-actions">overriding actions docs</a>.</p>
<h3 id="a-bundled-boost-skill">A bundled Boost skill</h3>
<p><a href="https://github.com/laravel/boost">Laravel Boost</a> is an MCP server from the Laravel team that exposes your application's installed packages and versions to your AI assistant, so prompts produce code that fits your stack instead of generic Laravel snippets. Packages can bundle their own Boost skills, and Boost discovers them automatically.</p>
<p>This package ships with a <a href="https://github.com/spatie/laravel-sluggable/blob/main/resources/boost/skills/sluggable-development/SKILL.md">sluggable-development skill</a> that walks your assistant through adding a sluggable model: picking the source field, deciding whether you need self-healing URLs, generating the migration, and dropping the attribute or trait on the model. If you have Boost installed, this is the easiest way to add a new sluggable model to your project.</p>
<h2 id="in-closing">In closing</h2>
<p>You can find the <a href="https://github.com/spatie/laravel-sluggable">package on GitHub</a> and the full <a href="https://spatie.be/docs/laravel-sluggable/v4">documentation on our docs site</a>. Coming from v3? The <a href="https://github.com/spatie/laravel-sluggable/blob/main/UPGRADING.md">upgrade guide</a> covers the breaking changes (mostly minimum versions and a <code>callable</code> to <code>Closure</code> migration).</p>
<p>This is one of <a href="https://spatie.be/open-source">many packages we've built at Spatie</a>. If you want to support our open source work, consider picking up one of our <a href="https://spatie.be/products">paid products</a>.</p>
]]>
            </summary>
                                    <updated>2026-04-29T16:36:36+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How to monitor your Laravel app for critical vulnerabilities using Oh Dear]]></title>
            <link rel="alternate" href="https://freek.dev/3114-how-to-monitor-your-laravel-app-for-critical-vulnerabilities-using-oh-dear" />
            <id>https://freek.dev/3114</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>This practical guide shows how to use spatie/laravel-health together with Oh Dear to detect vulnerable Composer dependencies in production and get alerted quickly. It also shows how adding composer audit in CI gives you an extra early warning layer.</p>


<a href='https://ohdear.app/news-and-updates/how-to-monitor-your-laravel-app-for-critical-vulnerabilities-using-oh-dear'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-29T14:30:25+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Lock a shared test database for parallel test processes]]></title>
            <link rel="alternate" href="https://freek.dev/3083-lock-a-shared-test-database-for-parallel-test-processes" />
            <id>https://freek.dev/3083</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>AI coding agents love to run tests in parallel processes.</p>
<p>That's great until multiple processes try to use the same local test database at once. A small file lock can serialize access and stop those runs from stepping on each other.</p>


<a href='https://rias.be/blog/lock-a-shared-test-database-for-parallel-test-processes'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-28T14:48:29+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Running GitHub Actions on a Mac Mini]]></title>
            <link rel="alternate" href="https://freek.dev/3082-running-github-actions-on-a-mac-mini" />
            <id>https://freek.dev/3082</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Matthieu Napoli explains how he replaced GitHub-hosted CI with self-hosted runners on a Mac Mini at home. The setup is simple, fast, and a good look at the trade-offs around isolation, caching, and parallelism.</p>


<a href='https://mnapoli.fr/running-github-actions-mac-mini'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-27T14:30:32+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Porting Mac OS X to the Nintendo Wii]]></title>
            <link rel="alternate" href="https://freek.dev/3081-porting-mac-os-x-to-the-nintendo-wii" />
            <id>https://freek.dev/3081</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>An absurd and very fun technical deep dive into getting Mac OS X 10.0 running natively on a Nintendo Wii. It covers the hardware investigation, boot process, kernel patching, and driver work needed to pull it off.</p>


<a href='https://bryankeller.github.io/2026/04/08/porting-mac-os-x-nintendo-wii.html'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-26T14:30:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Understanding Traceroute]]></title>
            <link rel="alternate" href="https://freek.dev/3079-understanding-traceroute" />
            <id>https://freek.dev/3079</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A clear walkthrough of how traceroute works under the hood. It explains the TTL trick, the ICMP replies routers send back, and even rebuilds the core idea in Rust.</p>


<a href='https://tech.stonecharioteer.com/posts/2026/traceroute/'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-25T14:30:26+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Enhancing our API for better agentic consumption]]></title>
            <link rel="alternate" href="https://freek.dev/3062-enhancing-our-api-for-better-agentic-consumption" />
            <id>https://freek.dev/3062</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>We've shipped several improvements to the Oh Dear API to make it work better with AI coding agents. The updates include historical check runs, dashboard links in every response, and markdown-friendly documentation.</p>


<a href='https://ohdear.app/news-and-updates/enhancing-our-api-for-better-agentic-consumption'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-24T14:30:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[★ Generate Apple and Google Wallet passes from Laravel]]></title>
            <link rel="alternate" href="https://freek.dev/3108-generate-apple-and-google-wallet-passes-from-laravel" />
            <id>https://freek.dev/3108</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>A mobile pass is that thing in your iPhone's Wallet app. A boarding pass, a concert ticket, a coffee loyalty card, a gym membership. Apple calls them passes. Google calls them objects. Both Wallet apps let you generate them, hand them out, and push live updates to the copy that's already on someone's device.</p>
<p>We just released <a href="https://github.com/spatie/laravel-mobile-pass">Laravel Mobile Pass</a>, a package that lets you generate those Apple and Google passes from a Laravel app and send updates to already issues passes.</p>
<p>Together with the package, we also published <a href="https://mobile-pass-demo.spatie.be">a demo site</a> where you can create Apple Wallet passes and push an update so you can see it all working on your own iOS device.</p>
<p><img src="https://freek.dev/admin-uploads/igrNGyZs64cOAtJMrdGV6zN0M6lxzFo2Gl1vJHB6.jpg" alt="" /></p>
<p><a href="https://github.com/danjohnson95">Dan Johnson</a> and I have been working on it for a while. Let me walk you through what it can do.</p>
<!--more-->
<h2 id="a-simple-example">A simple example</h2>
<p>Here's the shortest useful thing you can build with it. An Apple Wallet boarding pass for a flight:</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Spatie\LaravelMobilePass\Builders\Apple\AirlinePassBuilder</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Spatie\LaravelMobilePass\Enums\BarcodeType</span>;

<span class="hl-variable">$mobilePass</span> = <span class="hl-type">AirlinePassBuilder</span>::<span class="hl-property">make</span>()
    -&gt;<span class="hl-property">setOrganizationName</span>(<span class="hl-value">'Artisan Airways'</span>)
    -&gt;<span class="hl-property">setSerialNumber</span>(<span class="hl-value">'ART-103'</span>)
    -&gt;<span class="hl-property">setDescription</span>(<span class="hl-value">'Boarding Pass'</span>)
    -&gt;<span class="hl-property">addHeaderField</span>(<span class="hl-value">'flight-no'</span>, <span class="hl-value">'ART103'</span>, <span class="hl-property">label</span>: <span class="hl-value">'Flight'</span>)
    -&gt;<span class="hl-property">addHeaderField</span>(<span class="hl-value">'seat'</span>, <span class="hl-value">'66F'</span>)
    -&gt;<span class="hl-property">addField</span>(<span class="hl-value">'departure'</span>, <span class="hl-value">'ABU'</span>, <span class="hl-property">label</span>: <span class="hl-value">'Abu Dhabi International'</span>)
    -&gt;<span class="hl-property">addField</span>(<span class="hl-value">'destination'</span>, <span class="hl-value">'LHR'</span>, <span class="hl-property">label</span>: <span class="hl-value">'London Heathrow'</span>)
    -&gt;<span class="hl-property">addSecondaryField</span>(<span class="hl-value">'name'</span>, <span class="hl-value">'Mary Smith'</span>)
    -&gt;<span class="hl-property">setBarcode</span>(<span class="hl-type">BarcodeType</span>::<span class="hl-property">Pdf417</span>, <span class="hl-value">'ART-103-66F'</span>)
    -&gt;<span class="hl-property">save</span>();
</pre>
<p>Calling <code>save()</code> gives you back a <code>MobilePass</code> model. Nothing is written to disk. The whole pass (fields, images, barcode, the lot) lives as a row in the <code>mobile_passes</code> table that ships with the package.</p>
<h2 id="handing-the-pass-to-a-user">Handing the pass to a user</h2>
<p>So in the example above, the <code>$mobilePass</code> variable contains <code>MobilePass</code> Eloquent model. It plays nice with the parts of Laravel you already know. Here are a few ways you can use it to send a mobile pass to a user</p>
<p>Return it straight from a controller. The model implements <code>Responsable</code>, so the package takes care of signing and serving the <code>.pkpass</code> file.</p>
<pre data-lang="php" class="notranslate"><span class="hl-comment">// in a controller</span>
<span class="hl-keyword">return</span> <span class="hl-variable">$mobilePass</span>;
</pre>
<p>Attach it to an outgoing mail. The model also implements <code>Attachable</code>, which means you can drop it into a Mailable's <code>attachments()</code> method and Laravel figures out the rest.</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">class</span> <span class="hl-type">FlightBooked</span> <span class="hl-keyword">extends</span> <span class="hl-type">Mailable</span>
{
    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">__construct</span>(<span class="hl-injection"><span class="hl-keyword">public</span> <span class="hl-type">MobilePass</span> <span class="hl-property">$mobilePass</span></span>) {}

    <span class="hl-keyword">public</span> <span class="hl-keyword">function</span> <span class="hl-property">attachments</span>(): <span class="hl-type">array</span>
    {
        <span class="hl-keyword">return</span> [<span class="hl-variable">$this</span>-&gt;<span class="hl-property">mobilePass</span>];
    }
}
</pre>
<p>The user taps through, sees the pass preview in Apple Wallet, and taps Add.</p>
<p><img src="https://freek.dev/admin-uploads/Ohr1KpMoDzxonwUDTHQJ0XHrEf9rrxm7b6O4mjYu.png" alt="" /></p>
<p>Apple then calls back to your app to register the device against the pass. The package handles that endpoint and stores the registration. That link between pass and device is what lets you push updates later.</p>
<h2 id="pushing-a-live-update">Pushing a live update</h2>
<p>Here's the part I like most. If a seat gets reassigned or a gate changes, you can update the pass from your app and the version on the user's phone updates within a minute, without them doing anything.</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$mobilePass</span>-&gt;<span class="hl-property">updateField</span>(<span class="hl-value">'seat'</span>, <span class="hl-value">'13A'</span>);
</pre>
<p>Under the hood, the package dispatches a job that notifies Apple through APNs. Apple pings the device. The device fetches the new version from your server. Wallet swaps the old pass for the new one.</p>
<p>Now this all happens silently, without warning the user.  If you want the user to see a notification when the value changes, pass a <code>changeMessage</code>:</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$mobilePass</span>-&gt;<span class="hl-property">updateField</span>(
    <span class="hl-value">'seat'</span>,
    <span class="hl-value">'13A'</span>,
    <span class="hl-property">changeMessage</span>: <span class="hl-value">'Your seat was changed to :value'</span>,
);
</pre>
<p><img src="https://freek.dev/admin-uploads/NX0iBKbdWswZbKHeQkc5dZPTUyc2xe4jJEO2xxkB.jpg" alt="" /></p>
<p>Very cool! If you tap that notification, you,'ll notice that Passbook highlights what was changed.</p>
<p><img src="https://freek.dev/admin-uploads/xJWfuSJ1qgBA7EkGPCfTdAqzMgdjLN4lwpPhYYra.jpg" alt="" /></p>
<h2 id="google-wallet-is-supported-too">Google Wallet is supported too</h2>
<p>Google Wallet works a little differently. Google wants you to declare a Class once (a shared template for a batch of passes) and then issue individual Objects against that class. The package covers both.</p>
<pre data-lang="php" class="notranslate"><span class="hl-keyword">use</span> <span class="hl-type">Spatie\LaravelMobilePass\Builders\Google\EventTicketPassBuilder</span>;
<span class="hl-keyword">use</span> <span class="hl-type">Spatie\LaravelMobilePass\Enums\BarcodeType</span>;

<span class="hl-variable">$mobilePass</span> = <span class="hl-type">EventTicketPassBuilder</span>::<span class="hl-property">make</span>()
    -&gt;<span class="hl-property">setClass</span>(<span class="hl-value">'spring-tour-2026'</span>)
    -&gt;<span class="hl-property">setAttendeeName</span>(<span class="hl-value">'Mary Smith'</span>)
    -&gt;<span class="hl-property">setSection</span>(<span class="hl-value">'Floor A'</span>)
    -&gt;<span class="hl-property">setRow</span>(<span class="hl-value">'12'</span>)
    -&gt;<span class="hl-property">setSeat</span>(<span class="hl-value">'24'</span>)
    -&gt;<span class="hl-property">setBarcode</span>(<span class="hl-type">BarcodeType</span>::<span class="hl-property">Qr</span>, <span class="hl-value">'TICKET-12345'</span>)
    -&gt;<span class="hl-property">save</span>();
</pre>
<p>Returning <code>$mobilePass</code> from a controller does the right thing on either platform. Android users get redirected to the Google Wallet save URL. iPhone users get the <code>.pkpass</code> download. The <code>Responsable</code> implementation picks the right response for the platform the pass was built for.</p>
<h2 id="a-few-more-things-worth-knowing">A few more things worth knowing</h2>
<p>Picture the boarding pass from earlier. You hand it to the user three days before the flight. They install it and promptly forget it exists. An hour before departure, or the moment they walk into the airport, the pass drifts onto their lock screen without them doing anything. By the time they reach the gate, their thumb is already on it.</p>
<p>That trick is called pass relevance, and wiring it up is two calls on the builder:</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$builder</span>
    -&gt;<span class="hl-property">addLocation</span>(<span class="hl-property">latitude</span>: 25.2528, <span class="hl-property">longitude</span>: 55.3644)
    -&gt;<span class="hl-property">setRelevantDate</span>(<span class="hl-variable">$flight</span>-&gt;<span class="hl-property">departs_at</span>);
</pre>
<p>The location turns on the geofence. The date turns on the clock. Combine both and Wallet shows the pass at the right place and the right moment. It's the small thing that makes a Wallet pass feel like it belongs on the device instead of inside an email.</p>
<p>Passes don't need to stick around once they've served their purpose. When a coupon has been redeemed or a ticket is from last night, call <code>expire()</code> on the model and Wallet greys it out and drops it from the lock screen:</p>
<pre data-lang="php" class="notranslate"><span class="hl-variable">$mobilePass</span>-&gt;<span class="hl-property">expire</span>();
</pre>
<p>If you issue Google Wallet passes, you probably want to know when someone actually saves one to their Wallet, or later removes it. Google pings your app on both events. The package listens for those callbacks and fires regular Laravel events you can subscribe to:</p>
<pre data-lang="php" class="notranslate"><span class="hl-type">Event</span>::<span class="hl-property">listen</span>(<span class="hl-type">GoogleMobilePassSaved</span>::<span class="hl-keyword">class</span>, <span class="hl-keyword">function</span> (<span class="hl-injection"><span class="hl-type">GoogleMobilePassSaved</span> $event</span>) {
    <span class="hl-comment">// $event-&gt;mobilePass is now on the user's Wallet</span>
});
</pre>
<p>Drop in a listener if you want to record activations, send a thank-you email, or kick off any other flow. It's the kind of integration that would otherwise involve reading Google's callback spec and signing JWTs by hand. You shouldn't have to.</p>
<h2 id="try-it-live">Try it live</h2>
<p>We put up a demo at <a href="https://mobile-pass-demo.spatie.be">mobile-pass-demo.spatie.be</a>. Pick a pass type on the landing page, scan the QR code with your iPhone, install the pass, and hit the simulate-change button. Your Wallet pulls the new version a moment later. The full source of the demo app is on <a href="https://github.com/spatie/laravel-mobile-pass-demo">GitHub</a> if you want to see how everything ties together.</p>
<p><img src="https://freek.dev/admin-uploads/igrNGyZs64cOAtJMrdGV6zN0M6lxzFo2Gl1vJHB6.jpg" alt="" /></p>
<h2 id="in-closing">In closing</h2>
<p>At Laracon India 2025, <a href="https://github.com/danjohnson95">Dan Johnson</a> showed me a prototype he had been hacking on for generating Apple Wallet passes. I loved the idea, we decided to team up, and this package is what came out the other end. It took a while to get here, but we finally gave it the polish we wanted.</p>
<p>Mobile passes aren't new. Apple shipped Wallet (back then called Passbook) with iOS 6 in 2012, and Google followed with their own Wallet passes a few years later.</p>
<p>Given the two have been around for that long, I was surprised nobody had built a solid Laravel package around either platform yet. Luckily there's one now. Hope you like it!</p>
<p>The code is on <a href="https://github.com/spatie/laravel-mobile-pass">GitHub</a>. The full documentation is at <a href="https://spatie.be/docs/laravel-mobile-pass">spatie.be</a>.</p>
<p>This is one of the many packages we've created at Spatie. If you want to support our open source work, consider picking up one of our <a href="https://spatie.be/open-source/support-us">paid products</a>.</p>
]]>
            </summary>
                                    <updated>2026-04-23T16:00:19+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[In Praise of --dry-run]]></title>
            <link rel="alternate" href="https://freek.dev/3077-in-praise-of-dry-run" />
            <id>https://freek.dev/3077</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Henrik Warne makes a good case for adding a --dry-run mode to commands that change state. It gives you a fast, safe way to verify configuration, inspect behavior, and test workflows without side effects.</p>


<a href='https://henrikwarne.com/2026/01/31/in-praise-of-dry-run/'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-23T14:30:27+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How Will LLMs Transform Us? AI as a Tool in the Future of Development]]></title>
            <link rel="alternate" href="https://freek.dev/3076-how-will-llms-transform-us-ai-as-a-tool-in-the-future-of-development" />
            <id>https://freek.dev/3076</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>This article frames AI as a tool to support, not replace, developers, emphasizing the importance of staying in control of how and when it’s used. It encourages a thoughtful approach where developers leverage AI for efficiency while maintaining ownership of decisions and outcomes.</p>


<a href='https://tighten.com/insights/pragmatic-ai-ai-as-a-tool/'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-22T14:04:26+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[What Paddle doesn't tell you about implementing metered billing]]></title>
            <link rel="alternate" href="https://freek.dev/3075-what-paddle-doesnt-tell-you-about-implementing-metered-billing" />
            <id>https://freek.dev/3075</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>How to implement calendar-month metered billing on Paddle using zero-value subscriptions, one-time charges, and a homemade invoice grace period.</p>


<a href='https://phare.io/blog/what-paddle-doesnt-tell-you-about-implementing-metered-billing/'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-21T14:11:26+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Introducing TypeScript Transformer 3]]></title>
            <link rel="alternate" href="https://freek.dev/3094-introducing-typescript-transformer-3" />
            <id>https://freek.dev/3094</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Ruben explains the full rewrite behind TypeScript Transformer 3 and why the new architecture makes the package much more flexible. The post covers the new transformer pipeline, AST-based output, improved type parsing via PHPStan, and a watch mode for faster development.</p>


<a href='https://rubenvanassche.com/p/c07a798b-01fe-41d3-a5c5-08b24789f4ac/'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-20T14:30:26+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Talking about Laravel, Oh Dear, and AI]]></title>
            <link rel="alternate" href="https://freek.dev/3073-talking-about-laravel-oh-dear-and-ai" />
            <id>https://freek.dev/3073</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>In this interview, I talk about Laravel, application monitoring, and how AI is changing the way developers work.</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/kNmnNoz7AWU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
]]>
            </summary>
                                    <updated>2026-04-19T14:30:30+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Scotty vs Laravel Envoy: Spatie's New Deploy Tool Is Worth the Switch]]></title>
            <link rel="alternate" href="https://freek.dev/3072-scotty-vs-laravel-envoy-spaties-new-deploy-tool-is-worth-the-switch" />
            <id>https://freek.dev/3072</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Hafiz compares Scotty with Laravel Envoy and explains why Spatie's new deploy tool is a nicer fit for SSH-based deployments. He walks through the plain bash format, improved terminal output, migration path, and zero-downtime deployment workflow.</p>


<a href='https://hafiz.dev/blog/scotty-vs-laravel-envoy-spatie-deploy-tool'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-18T14:30:30+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Spatie Guidelines as AI Skills]]></title>
            <link rel="alternate" href="https://freek.dev/3098-spatie-guidelines-as-ai-skills" />
            <id>https://freek.dev/3098</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>We turned our internal coding guidelines into reusable AI skills, so coding assistants can follow the same conventions their team uses. The package works with Laravel Boost and the broader skills.sh ecosystem, and ships with skills for Laravel PHP, JavaScript, version control, and security.</p>


<a href='https://spatie.be/blog/spatie-guidelines-as-ai-skills'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-17T14:30:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[Validating Array Inputs in Laravel Without the N+1]]></title>
            <link rel="alternate" href="https://freek.dev/3070-validating-array-inputs-in-laravel-without-the-n1" />
            <id>https://freek.dev/3070</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[<p>Validate nested array inputs in Laravel form requests without the N+1. Prefetch lookup data in prepareForValidation and check items in memory.</p>


<a href='https://daryllegion.com/validating-array-inputs-in-laravel-without-the-n-plus-1'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-16T14:22:28+02:00</updated>
        </entry>
            <entry>
            <title><![CDATA[How 10x Developers Actually Use AI]]></title>
            <link rel="alternate" href="https://freek.dev/3067-how-10x-developers-actually-use-ai" />
            <id>https://freek.dev/3067</id>
            <author>
                <name><![CDATA[Freek Van der Herten]]></name>
                <email><![CDATA[freek@spatie.be]]></email>

            </author>
            <summary type="html">
                <![CDATA[

<a href='https://youtu.be/_78-xwTyQeE?si=6SvmgiF7-HHig0gs'>Read more</a>]]>
            </summary>
                                    <updated>2026-04-15T14:03:26+02:00</updated>
        </entry>
    </feed>
