Freek Van der Herten
banner
freek.bsky.social
Freek Van der Herten
@freek.bsky.social
PHP developer at Spatie, built Mailcoach, ‪myray.app‬, ohdear.app and flareapp.io, blogging at ‪freek.dev‬, organising fullstackeurope.com
🌟 I built a native mobile word game in two weeks
I built a native mobile word game in two weeks
At Laracon India, I launched [a major update of Ray](https://myray.app). For that talk, I needed a little demo project to showcase Ray. I built a simple website about a then-fictional mobile app to play a Scrabble-like word game called [WordStockt](https://wordstockt.com). But then I got curious: how far could I push AI-assisted development? Could I actually just create the whole game? After about 10 days, WordStockt is a fully functional word game that's 98% vibe-coded. It's available [for iOS](https://apps.apple.com/app/wordstockt/id6757525145) and [Android](https://play.google.com/store/apps/details?id=com.wordstockt.app). In this post, I'd like to tell you more about it. ![](https://freek.dev/admin-uploads/XRbFtLQeyuQQESw2uIemo4E7LlWAIVexIAeniV24.jpg) ## From Demo to Real App WordStockt is probably the most elaborate demo I've ever built for a talk. It's a classic word game (think Scrabble) where you can challenge friends to asynchronous matches, track your stats, and climb the leaderboards. You can play at your own pace: make a move, close the app, and come back when your opponent has played. Push notifications let you know when it's your turn. Before writing any code, I started describing the main functionalities of the app in a markdown file. First, I let the AI interview me about the requirements I had written to clarify some details. Next, I instructed the AI to split up the work into multiple phases, each with its own detailed markdown file. After I refined those markdowns further, I instructed the AI to start building it phase by phase. Since I know Laravel quite well, I was reasonably sure I could guide the AI well enough to produce good results for the backend. I reviewed the code that it produced, and pushed it to use the patterns and code organization that I like. For the front-end, I was a little bit more nervous. According to my front-end colleagues at Spatie, the best way of building a native app from a single codebase was Expo / React Native. I didn't have experience with either of those (I do know a bit of regular React), so I couldn't always tell if the AI produced quality results. The code it produced always did work however. Something that I really am not accustomed to doing myself is coming up with good design. Sure, I can do some basic styling with Tailwind, but that's about it. To start with the design, I let the AI propose and show me ideas using Ray. AI also assisted with non-coding talks writing copy, creating app icons, and taking screenshots for various app stores (showing in screenshot below). ![](https://freek.dev/admin-uploads/J3RuH5kXUE2oB146q1DU5IpTxdlP3eBxZlIdqqRW.jpg) When reading the above, you might think that all things were built sequentially, but in reality I was doing things at the same time. In the screenshot belong, you can see me using one Claude session working on animations, and in another one, I'm letting the AI work an icons and letting it preview them in [Ray](https://myray.app). ![](https://freek.dev/admin-uploads/xy1ZEQ4EiGrXKFBq9IVZpFqswPribdgXFRTyT9hG.jpg) In total, it took me about 10 days to build this app from start to finish. That includes the design, the backend Laravel API, the apps, the website, and setting up all services. ## WordStockt is open source I've decided to open source the entire project. It's a good example of a modern Laravel API powering a React Native mobile app, and I hope others can learn from it. The Laravel backend API is available at [github.com/spatie/wordstockt.com](https://github.com/spatie/wordstockt.com). The production version of this app is hosted on [Forge](https://forge.laravel.com). For real-time updates, so you instantly see when your opponent plays, I'm using [Reverb](https://reverb.laravel.com). The iOS and Android apps are built from a single [React Native](https://reactnative.dev) codebase. You'll find the code in this repo: [github.com/spatie/wordstockt-app](https://github.com/spatie/wordstockt-app). Also noteworthy is that [Expo](https://expo.dev) is used to send native push notifications. PRs to these codebases are welcome! If you find bugs, want to add features, or improve the AI's code (there's probably room for that), feel free to contribute. ## What does this all mean Building WordStockt in 10 days would have been impossible for me without AI. The productivity gains are real and significant. I built a complete product across technologies I don't fully master, in a fraction of the time it would have taken otherwise. Honestly, previously I couldn't have built this without the help of my team members. That said, I don't think this replaces the need to understand what you're building. Or to have passionate fellow developers around you. I could guide the Laravel side confidently because I know Laravel. The React Native side worked, but I have less confidence in its quality because I can't fully evaluate it. AI amplifies what you already know more than it substitutes what you don't. Therefore, like argued in my talk at Laracon India, I think it will remain important to invest in technical skills and knowledge. Where this goes, I honestly don't know. Some call this [the rise of industrial software](https://chrisloy.dev/post/2025/12/30/the-rise-of-industrial-software), comparing it to how manufacturing transformed other industries. There will be challenges for junior developers trying to learn, for teams figuring out how to do [code review when AI generates code faster than humans can review it](https://tidyfirst.substack.com/p/party-of-one-for-code-review), and for all of us adapting to a faster pace of change. But I'm optimistic. My buddy Mattias wrote that AI makes [web development fun again](https://ma.ttias.be/web-development-is-fun-again/) and that it's [no longer optional](https://ma.ttias.be/ai-no-longer-optional/). I fully agree with both. The barrier to building things is lower than ever. Ideas that would have stayed ideas because "I don't have time" or "I don't know that stack" are now within reach. That feels like an opportunity worth embracing. ## In closing WordStockt is completely free with no ads. I built it for fun and as a demo, not to make money. Download it now on the [App Store](https://apps.apple.com/app/wordstockt/id6757525145) or [Google Play](https://play.google.com/store/apps/details?id=com.wordstockt.app). ![](https://freek.dev/admin-uploads/tKJjTZCWGoHShM75n5yLI9DUwF1IaY7YuCdP9KmQ.png)
freek.dev
January 31, 2026 at 11:50 AM
🔗 How to automatically generate a commit message using Claude
How to automatically generate a commit message using Claude
For years, my git history contains "wip" commit messages. I don't really often use git history myself, but my colleagues do. And when they're trying to understand a change I made six months ago, "wip" tells them absolutely nothing. Might as well not have commit messages at all. I knew I should write better commit messages, but the friction was real. Stopping to think about how to summarize my changes felt like it broke my flow. So I kept typing "wip". I added a bash function to my dotfiles that uses Claude to generate commit messages for me. ![](https://freek.dev/admin-uploads/hUISpOkDrkaEgefakRGH7urz7torFU7nWRNY7FJM.jpg) ## My commit function Here's the core of it the function: ```bash function commit() { commitMessage="$*" git add . if [ "$commitMessage" = "" ]; then diff_input=$(echo "=== Summary ===" && git diff --cached --stat && echo -e "\n=== Diff (truncated if large) ===" && git diff --cached | head -c 50000) commitMessage=$(echo "$diff_input" | claude -p "Write a single-line commit message for this diff. Output ONLY the message, no quotes, no explanation, no markdown.") git commit -m "$commitMessage" return fi eval "git commit -a -m '${commitMessage}'" } ``` If I call `commit` with no arguments, it stages everything and asks Claude to generate a commit message based on the diff. If I pass in a message like `commit "Fix bug in auth"`, it uses that instead. ## What are dotfiles anyway? Quick sidebar: "dotfiles" are configuration files that live in your home directory and typically start with a dot (like `.zshrc` or `.bashrc`). They control how your terminal, shell, and various command-line tools behave. Many developers (myself included) keep their dotfiles in a git repository so they can sync their entire development environment across machines. If I get a new laptop, I just clone my [dotfiles repo](https://github.com/freekmurze/dotfiles) and I'm back to my familiar setup in minutes. In my case, I have a [`.functions`](https://github.com/freekmurze/dotfiles/blob/main/home/.functions) file in my dotfiles that contains custom bash functions like this `commit` function. My `.zshrc` sources that file, so these functions are available in every terminal session. ## How the AI generation works The interesting part is what gets sent to Claude. I don't just send the raw diff - that could be huge and contain too much noise. Instead, I send two things: 1. A summary: `git diff --cached --stat` shows which files changed and how many lines were added/removed 2. The actual diff: But truncated to 50,000 characters to avoid overwhelming the AI with massive diffs Then I pass it to the `claude` CLI with a simple prompt: "Write a single-line commit message for this diff. Output ONLY the message, no quotes, no explanation, no markdown." The result? Commit messages that actually describe what changed: - `Add caching layer to user repository` - `Fix N+1 query in post index` - `Remove deprecated payment gateway integration` Instead of: - `wip` - `wip` - `wip` ## Still fast, still flexible The whole thing takes maybe 2-3 seconds. I type `commit`, and my changes are committed with a descriptive message. If I want to override it with my own message, I just pass it as an argument: `commit "Fix authentication bug"`. The function detects that I've provided a message and uses that instead of generating one. Best of both worlds. ## Adding a spinner for nicer output The function works fine as-is, but waiting 2-3 seconds staring at a blank terminal feels longer than it actually is. So I added a spinner animation to give some visual feedback. Here's the full version with the spinner: ```bash function commit() { commitMessage="$*" git add . if [ "$commitMessage" = "" ]; then # Start spinner in background { spinner="⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" while true; do for (( i=0; i<${#spinner}; i++ )); do printf "\r${spinner:$i:1} Generating commit message..." sleep 0.1 done done } &! spinner_pid=$! # Cleanup function for interrupt cleanup() { { kill $spinner_pid; wait $spinner_pid; } 2>/dev/null printf "\r\033[K" trap - INT return 1 } trap cleanup INT # Get diff and generate message diff_input=$(echo "=== Summary ===" && git diff --cached --stat && echo -e "\n=== Diff (truncated if large) ===" && git diff --cached | head -c 50000) commitMessage=$(echo "$diff_input" | claude -p "Write a single-line commit message for this diff. Output ONLY the message, no quotes, no explanation, no markdown.") # Stop spinner and clear line trap - INT { kill $spinner_pid; wait $spinner_pid; } 2>/dev/null printf "\r\033[K" git commit -m "$commitMessage" return fi eval "git commit -a -m '${commitMessage}'" } ``` Now when I run `commit`, I see: ![](https://freek.dev/admin-uploads/Fk8DMNcvmIz2dyO2UvNRNEukE4ONQGPCZeoGnGl5.jpg) It's purely aesthetic, but it makes the tool feel more polished. Little details like this turn a script into something that feels good to use. ## In closing This is a small automation, but it's made me a better teammate. My commit history is now actually useful for the people who do read it. When my colleagues are tracking down when a feature was added or trying to understand why something changed, they can scan through my commits and find what they're looking for. And I never have to type "wip" again. If you want to use this yourself, you can find the [full `commit` function in my dotfiles](https://github.com/freekmurze/dotfiles/blob/main/home/.functions). You'll need the [Claude CLI](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo) installed, but once you have that, just drop the function into your shell config and you're good to go.
freek.dev
January 26, 2026 at 8:56 AM
🔗 Running PHP 8.5 with Laravel Octane and FrankenPHP: The Missing Manual (Fix the 8.4 binary trap)
Running PHP 8.5 with Laravel Octane and FrankenPHP: The Missing Manual (Fix the 8.4 binary trap)
A quick but essential fix for anyone pushing the edges of the Laravel ecosystem. The default FrankenPHP binary provided by artisan octane:install is static and locked to PHP 8.4, which causes major headaches if your application relies on PHP 8.5 features or specific system extensions.
danielpetrica.com
January 21, 2026 at 1:38 PM
🔗 From dd() to Ray: A Debugging Workflow That Doesn't Break Your Flow
From dd() to Ray: A Debugging Workflow That Doesn't Break Your Flow
Evolving from dd() to Ray for a smoother workflow. Explore how Ray can boost your debugging game without stopping your code!
tnakov.dev
January 20, 2026 at 1:18 PM
🔗 From dd() to Ray: A Debugging Workflow That Doesn't Break Your Flow
From dd() to Ray: A Debugging Workflow That Doesn't Break Your Flow
I fully agree (but I might not be totally objective here 🙂)
tnakov.dev
January 19, 2026 at 1:08 PM
🔗 Livewire 4 Deep Dive: Components, Performance & New Directives
Livewire 4 Deep Dive: Components, Performance & New Directives
Livewire 4 introduces powerful new features that make building Laravel applications even better.
youtu.be
January 16, 2026 at 1:50 PM
🔗 Behind the Terminal
Behind the Terminal
Why I made my portfolio feel like a terminal, and how it works
gusk.ca
January 15, 2026 at 1:58 PM
🔗 What gets lost when everything is effortless?
What gets lost when everything is effortless?
Not all friction is bad.
carlbarenbrug.com
January 14, 2026 at 1:02 PM
🔗 Symfony 20 year!
Symfony 20 year!
This year, Symfony celebrates its 20 year anniversary. Let’s dive into some statistics of years of making web development history.
wouterj.nl
January 12, 2026 at 1:37 PM
🔗 The rise of industrial software
The rise of industrial software
The open question, then, is not whether industrial software will dominate, but what that dominance does to the surrounding ecosystem.
chrisloy.dev
January 9, 2026 at 1:35 PM
🔗 Party of One for Code Review
Party of One for Code Review
Some good thoughts on code reviews in the emerging age of AI.
tidyfirst.substack.com
January 8, 2026 at 1:33 PM
🔗 Eval Testing LLMs in PHPUnit
Eval Testing LLMs in PHPUnit
Prompts break silently. Here's how to catch regressions with PHPUnit evals before your users do.
joshhornby.com
January 5, 2026 at 1:42 PM
🔗 Build an AI-Powered Drawing Guessing Game with Laravel, Prism, and HTML Canvas
Build an AI-Powered Drawing Guessing Game with Laravel, Prism, and HTML Canvas
Learn how to build an AI-powered drawing guessing game. In this little app, users will draw anything they like, and the AI will try to guess what it is.
tighten.com
December 30, 2025 at 1:31 PM
🔗 Partial Function Application is coming in PHP 8.6
Partial Function Application is coming in PHP 8.6
Partial Function Application in PHP 8.6 will let you write a “pre‑configured” callable by calling a function with some arguments and using placeholders for the rest.
www.amitmerchant.com
December 29, 2025 at 11:56 AM
Reposted by Freek Van der Herten
We're almost ready to sign off for the year. It's been a productive and exciting year for us. If you're interested, you can read our year-end review email here: spatie.mailcoach.app/webview/camp.... Wishing you happy holidays from everyone at Spatie!
December 19, 2025 at 12:58 PM
Reposted by Freek Van der Herten
📢 Sponsor Announcement!

We’re excited to welcome Ray by @spatie.be as a Gold Sponsor for #LaraconIN 2026.

Debug Laravel faster with Ray. Beautiful UI for inspecting variables, queries, mails and much more! myray.app
December 12, 2025 at 6:08 AM
🔗 Laravel and Traefik: Dynamic Configuration for Effortless Multi-Domain Management
Laravel and Traefik: Dynamic Configuration for Effortless Multi-Domain Management
Streamline dynamic multi-domain routing in Laravel with Traefik by serving YAML via the HTTP provider for automated, secure, and scalable configuration management. Ideal for Laravel developers and DevOps teams who want to eliminate bulky static files and improve maintainability.
coz.jp
December 11, 2025 at 1:17 PM
🔗 A Production-Ready Laravel Architecture with Traefik and FrankenPHP
A Production-Ready Laravel Architecture with Traefik and FrankenPHP
A practical guide to deploying a high-performance Laravel stack using Octane, FrankenPHP, and a fully automated Docker Compose workflow.
coz.jp
December 9, 2025 at 1:05 PM
🔗 How do arrays work?
How do arrays work?
This post dives into the technical details of the array and figure out how you might invent the array yourself.
nan-archive.vercel.app
December 5, 2025 at 2:00 PM
🔗 The $1,000 AWS mistake
The $1,000 AWS mistake
A cautionary tale about AWS VPC networking, NAT Gateways, and how a missing VPC Endpoint turned S3 data transfers into an expensive lesson.
www.geocod.io
December 4, 2025 at 1:58 PM