<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>afonso jorge ramos</title><description>Product-minded software engineer and tech lead. Most of my work is full-stack TypeScript, and I&apos;m framework-agnostic: most at home in React, but just as comfortable shipping in Svelte, Solid, Vue, or Angular. I ship across languages just as readily: I core-maintain Spicetify, a Go CLI with 20M+ downloads and 23k+ GitHub stars, and I brought macOS support to qbz, a bit-perfect hi-fi audio player written in Rust, upstreaming fixes to it and to the Rust audio crates it builds on, like coreaudio-rs and notify-rust.</description><link>https://afonsojramos.me/</link><item><title>You Can Just Do Things</title><link>https://afonsojramos.me/blog/rooting-smart-tvs/</link><guid isPermaLink="true">https://afonsojramos.me/blog/rooting-smart-tvs/</guid><description>I hate my remote&apos;s preset app buttons: press one and the TV installs the service. The solution was rooting it, which snowballed into a webOS app I did not plan to build. Without AI making the attempt cheap, I would have just never connected the TV.</description><pubDate>Fri, 28 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You could always just do things. What AI changed is the time an attempt costs, and time was most of the decision of whether to do something. If you only want that argument, it is &lt;a href=&quot;#the-route-i-would-have-taken&quot;&gt;at the end&lt;/a&gt;; everything between here and there is the evidence.&lt;/p&gt;
&lt;p&gt;This started with one of those things that bugs you every day until you break.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/buffering.D_AXDlZP_Z1yiu7x.webp&quot; alt=&quot;Plex buffering while playing Silo&quot;&gt;&lt;figcaption&gt;Plex buffering while playing Silo&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Plex would play for a while on my Xiaomi TV Box S 3rd Gen, stop to buffer, recover, and eventually become annoying enough that I restarted the box. The restart usually helped and mostly avoided the problem, which made the diagnosis feel obvious: the hardware was underpowered, Xiaomi had filled Android with too much garbage, or the firmware had reached the point where replacing it would be easier than understanding it.&lt;/p&gt;
&lt;p&gt;I started looking for custom ROMs. I wondered whether rooting the box would make it faster. I was already thinking about replacing it with a more easily rootable Android TV device and rooting that instead.&lt;/p&gt;
&lt;p&gt;None of those things would have fixed the problem.&lt;/p&gt;
&lt;p&gt;But, by the time I finished this journey, the Xiaomi was still running its stock firmware and Plex no longer buffered. The more interesting work eventually moved to my LG C3, which was now rooted, stripped of several layers of optional data collection, and running a small webOS application I had not intended to build.&lt;/p&gt;
&lt;p&gt;As with &lt;a href=&quot;/blog/accidental-homelab&quot;&gt;my homelab&lt;/a&gt;, there was no plan connecting those outcomes. I was purely exploring how much better I could make the hardware I already owned, and each irritation led me to the next one.&lt;/p&gt;
&lt;nav class=&quot;table-of-contents&quot;&gt;&lt;ol&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#i-nearly-rooted-the-wrong-device&quot; title=&quot;I nearly rooted the wrong device&quot;&gt;I nearly rooted the wrong device&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#i-just-hate-all-tracking-and-advertising&quot; title=&quot;I just hate all tracking and advertising&quot;&gt;I just hate all tracking and advertising&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-lg-was-a-different-kind-of-problem&quot; title=&quot;The LG was a different kind of problem&quot;&gt;The LG was a different kind of problem&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-privacy-screen-was-only-one-layer&quot; title=&quot;The privacy screen was only one layer&quot;&gt;The privacy screen was only one layer&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#root-does-not-make-old-software-compatible&quot; title=&quot;Root does not make old software compatible&quot;&gt;Root does not make old software compatible&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-button-that-installed-rakuten&quot; title=&quot;The button that installed Rakuten&quot;&gt;The button that installed Rakuten&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#so-there-was-a-missing-ui&quot; title=&quot;So there was a missing UI&quot;&gt;So there was a missing UI&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#back-did-not-mean-back&quot; title=&quot;Back did not mean Back&quot;&gt;Back did not mean Back&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-volume-bar-that-would-not-go-away&quot; title=&quot;The volume bar that would not go away&quot;&gt;The volume bar that would not go away&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-route-i-would-have-taken&quot; title=&quot;The route I would have taken&quot;&gt;The route I would have taken&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#steering-into-the-rabbit-hole&quot; title=&quot;Steering into the rabbit hole&quot;&gt;Steering into the rabbit hole&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/nav&gt;&lt;h2 id=&quot;i-nearly-rooted-the-wrong-device&quot;&gt;I nearly rooted the wrong device&lt;a class=&quot;heading-anchor&quot; href=&quot;#i-nearly-rooted-the-wrong-device&quot; aria-label=&quot;Permalink to I nearly rooted the wrong device&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;My initial goal was to root the Android box. Plex was reporting Direct Play, and I had been treating that as evidence that the path between the server and the box was healthy, which left the box looking like the problem. I did suspect the router, since I am &lt;em&gt;temporarily&lt;/em&gt; on a cheap ISP-provided one, but I never thought it was &lt;em&gt;just&lt;/em&gt; because I was on 2.4 GHz instead of 5 GHz.&lt;/p&gt;
&lt;p&gt;But as soon as I started going deep into this, I noticed that the measurement that mattered was below Plex entirely. The Xiaomi had associated with the 2.4 GHz band of my combined network, and its average latency to the router, not to Plex, not to the Internet, but to the first hop across the room, was roughly &lt;strong&gt;1,323 ms&lt;/strong&gt;, with &lt;strong&gt;5% packet loss&lt;/strong&gt;. I added a 5 GHz-only SSID and moved the box onto it. Latency fell to &lt;strong&gt;4.65 ms&lt;/strong&gt; and packet loss to &lt;strong&gt;0%&lt;/strong&gt;. The same Plex file stopped buffering.&lt;/p&gt;
&lt;p&gt;Rooting the box would have been an impressively complicated way not to fix a bad wireless connection. That is the failure mode of cheap experiments: they are just as cheap to point at the wrong thing.&lt;/p&gt;
&lt;h2 id=&quot;i-just-hate-all-tracking-and-advertising&quot;&gt;I just hate all tracking and advertising&lt;a class=&quot;heading-anchor&quot; href=&quot;#i-just-hate-all-tracking-and-advertising&quot; aria-label=&quot;Permalink to I just hate all tracking and advertising&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The box still deserved a cleanup. Wireless debugging gave me ADB without opening anything or unlocking the bootloader, and I removed what I did not want: the Xiaomi telemetry packages (&lt;code&gt;com.xiaomi.statistic&lt;/code&gt;, &lt;code&gt;com.miui.tv.analytics&lt;/code&gt;), the preinstalled streaming apps, and the ad-filled Google TV launcher, which Projectivy replaced. Twenty packages uninstalled, twelve more disabled, nothing flashed.&lt;/p&gt;
&lt;p&gt;Four of the removals were &lt;code&gt;com.android.adservices.api&lt;/code&gt;, &lt;code&gt;com.android.sdksandbox&lt;/code&gt;, &lt;code&gt;com.android.ondevicepersonalization.services&lt;/code&gt;, and &lt;code&gt;com.android.federatedcompute.services&lt;/code&gt;. On this Android 14 build, &lt;code&gt;system_server&lt;/code&gt; crash-loops without them. The first time, the loop only surfaced at a cold boot after a power cut, and Android’s Rescue Party escalated to a factory reset: every removal reverted, and my own apps went with them. When I re-removed the same four during the second pass, the box reset immediately.&lt;/p&gt;
&lt;p&gt;They stay installed now. Of everything I deleted, from TalkBack to the calendar sync adapters, the packages this television box refused to live without were the advertising machinery. Anyways, blocking that traffic can live in the router firewall rules once I replace the ISP router with a UniFi Wi-Fi 7 one.&lt;/p&gt;
&lt;h2 id=&quot;the-lg-was-a-different-kind-of-problem&quot;&gt;The LG was a different kind of problem&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-lg-was-a-different-kind-of-problem&quot; aria-label=&quot;Permalink to The LG was a different kind of problem&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Fixing the Xiaomi should have ended the project. I mean, I already had Button Mapper on it, so its buttons were already custom: the Netflix button led to Plex and YouTube to SmartTube. But instead, it made me look at the LG C3 it was connected to and ask a different question: if I could remove the things I did not want from Android, how much of the LG experience could I make mine?&lt;/p&gt;
&lt;p&gt;The LG was not slow. I was bothered by the promotions, branded remote buttons, ACR (&lt;a href=&quot;https://en.wikipedia.org/wiki/Automatic_content_recognition&quot;&gt;automatic content recognition&lt;/a&gt;), opaque privacy controls, and the general feeling that buying the panel had not quite bought control over the computer attached to it. This is why I usually recommend that no one connects their TVs to the internet unless there is a specific firmware update to be made.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/magic-remote.DLCDneBp_1CjqF6.webp&quot; alt=&quot;The lower half of an LG Magic Remote, crowded with branded buttons for streaming services.&quot;&gt;&lt;figcaption&gt;The lower half of an LG Magic Remote, crowded with branded buttons for streaming services.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;This time, root access was relevant.&lt;/p&gt;
&lt;p&gt;The TV was running webOS 25 on firmware 33.31.68. I checked the exact model and firmware against the community compatibility data, read through &lt;a href=&quot;https://github.com/throwaway96/slopbro&quot;&gt;SlopBro&lt;/a&gt;, and used a recorded source revision rather than an unknown binary. There is no useful percentage for the risk of bricking a television this way. The exploit itself normally either works or fails; the more durable risk begins after it succeeds, when a root shell makes bad ideas possible.&lt;/p&gt;
&lt;p&gt;SlopBro installed and elevated Homebrew Channel 0.7.3. The icon appearing was encouraging, but it was not proof of root. A shell returning this was:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;uid=0(root) gid=0(root)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I then rebooted the TV and checked again. Root SSH returned automatically, the Homebrew startup hook ran, and the same shell still had UID 0. That distinguished persistent root from an application that happened to launch once.&lt;/p&gt;
&lt;p&gt;The first job after gaining access was reducing how much access I had created. I installed a dedicated Ed25519 public key, required key-based SSH authentication, disabled the unauthenticated Telnet service, and confirmed port 23 stayed closed after reboot. I also enabled Homebrew’s firmware-update block. Rooting a television and leaving a passwordless root service listening on the network would have been a strange definition of taking control.&lt;/p&gt;
&lt;h2 id=&quot;the-privacy-screen-was-only-one-layer&quot;&gt;The privacy screen was only one layer&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-privacy-screen-was-only-one-layer&quot; aria-label=&quot;Permalink to The privacy screen was only one layer&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Several visible settings were already off: home promotions, content recommendations, screensaver advertising, personalised recommendations, and customised advertising.&lt;/p&gt;
&lt;p&gt;The underlying consent state told a less reassuring story. The TV still recorded permission for marketing, data-partner processing, viewing-information collection, interest-based advertising, voice-information processing, shopping-related processing, and ACR/LivePlus-related processing. The mic button was the daily reminder: pressing it did not open a microphone, it opened a terms-and-conditions screen asking me to accept voice processing, a menu that does not have an option to refuse those terms :)&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/user-agreements.DtKYkYLt_ZM3n2m.webp&quot; alt=&quot;The LG User Agreements screen, asking for consent to viewing, voice, and advertising agreements.&quot;&gt;&lt;figcaption&gt;The LG User Agreements screen, asking for consent to viewing, voice, and advertising agreements.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;em&gt;LG’s User Agreements screen, spelling out that viewing data collected through ACR “can be used and shared with our third-party partners for advertising purposes”. Source: &lt;a href=&quot;https://www.nngroup.com/articles/physical-discs-streaming-experience/&quot;&gt;Nielsen Norman Group&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I saved the existing state under Homebrew’s persistent data directory, then used webOS’s own settings service to revoke the optional agreements. Network access and the essential service terms stayed enabled; the advertising, voice, shopping, partner-sharing, and additional-data flags did not. I read the values back through the same API and updated the user-facing agreement list so the interface and effective state agreed.&lt;/p&gt;
&lt;p&gt;Homebrew also redirected LG’s firmware-update hosts locally and mounted the standard telemetry upload queues read-only. Both mechanisms were verified again after reboot.&lt;/p&gt;
&lt;p&gt;That still does not justify saying the TV makes no requests to LG. Its built-in tools were not enough to capture and attribute every DNS request and payload. I could see connections involving Google, GitHub, Cloudflare, AWS-hosted infrastructure, and local casting services, but an IP address is not proof of a hostname or purpose. A local resolver with per-device query logging would be the next honest step.&lt;/p&gt;
&lt;p&gt;Root gave me enough leverage to disable known collection paths. It did not turn incomplete evidence into certainty.&lt;/p&gt;
&lt;h2 id=&quot;root-does-not-make-old-software-compatible&quot;&gt;Root does not make old software compatible&lt;a class=&quot;heading-anchor&quot; href=&quot;#root-does-not-make-old-software-compatible&quot; aria-label=&quot;Permalink to Root does not make old software compatible&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The first Homebrew disappointment made the same point from another direction. I installed Custom Screensaver, and it did nothing.&lt;/p&gt;
&lt;p&gt;The failure was not permissions, it was just that &lt;a href=&quot;https://repo.webosbrew.org/&quot;&gt;webosbrew&lt;/a&gt; does not have a way of detecting app compatibility with specific webOS versions. So, for example, Custom Screensaver 1.0.1 expected an old QML entry point at &lt;code&gt;com.webos.app.screensaver/qml/main.qml&lt;/code&gt;. webOS 25 had replaced that implementation with a compiled Flutter application, so the path simply did not exist. Aerial and the other available replacements depended on the same older architecture.&lt;/p&gt;
&lt;p&gt;But, having root meant I could inspect the failure clearly. In this case though, it did not mean forcing an old patch onto a new system was sensible, so I removed the inert application and left the stock screensaver alone. Next goal: disabling the branded remote buttons.&lt;/p&gt;
&lt;h2 id=&quot;the-button-that-installed-rakuten&quot;&gt;The button that installed Rakuten&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-button-that-installed-rakuten&quot; aria-label=&quot;Permalink to The button that installed Rakuten&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Now that I had restored internet access to my LG TV, the dedicated remote buttons went back to auto-installing services I do not use: Netflix, Prime Video, Disney+, Rakuten TV, and Alexa. LG Input Hook looked like the right tool for disabling them, so I configured the four streaming buttons through its web interface and was about to add Alexa.&lt;/p&gt;
&lt;p&gt;Then I pressed Rakuten, and the TV started installing Rakuten.&lt;/p&gt;
&lt;p&gt;The configuration was correct; the hook was not running. On webOS 25 its injector failed while resolving an internal glibc &lt;code&gt;dlopen&lt;/code&gt; symbol, so none of its saved rules could intercept anything.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/andrewfraley/magic_mapper&quot;&gt;Magic Mapper&lt;/a&gt; took a different approach. It grabbed the Magic Remote input device, consumed configured buttons, and forwarded everything else to webOS. I pinned and reviewed its source, started it interactively with only the five branded buttons disabled, and pressed Rakuten again. This time the log said &lt;code&gt;Button rakuten is disabled&lt;/code&gt;, and nothing opened.&lt;/p&gt;
&lt;p&gt;I added a guarded startup script so the mapper returned after reboot. Functionally, the problem was solved.&lt;/p&gt;
&lt;p&gt;Operationally, it was not.&lt;/p&gt;
&lt;p&gt;Magic Mapper was a Python script, a JSON configuration file, and a startup hook. It had no launcher tile, no status screen, and no way to remove itself from the television’s UI. Installing an invisible root service is easy. Leaving behind something another person can understand and safely undo is what makes it usable in a house where not everyone is technical (even if it is just a button mapper).&lt;/p&gt;
&lt;h2 id=&quot;so-there-was-a-missing-ui&quot;&gt;So there was a missing UI&lt;a class=&quot;heading-anchor&quot; href=&quot;#so-there-was-a-missing-ui&quot; aria-label=&quot;Permalink to So there was a missing UI&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I initially imagined a small manager showing whether Magic Mapper was running, which buttons were blocked, and one large removal button. Once I looked further at the upstream feature set, that scope stopped making sense.&lt;/p&gt;
&lt;p&gt;Magic Mapper could already adjust OLED brightness, change energy-saving and eye-comfort modes, turn off the panel, launch applications, simulate other remote buttons, send IR and HDMI-CEC commands, call webhooks, open TCP connections, toggle PicCap, and disable the Magic Remote pointer. So, huge props for that feature set! A UI exposing only the five actions I happened to need would make the underlying project look much smaller than it was.&lt;/p&gt;
&lt;p&gt;The result became &lt;a href=&quot;https://github.com/afonsojramos/magic-mapper-webos&quot;&gt;Magic Mapper for webOS&lt;/a&gt;: a standalone Homebrew application around a pinned, checksummed upstream runtime. It can discover a button while suppressing its normal action, present every upstream action through remote-friendly categories, validate the inputs, show authoritative runtime status, restore individual mappings, and remove its own state and startup hook cleanly.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/remote-buttons.DGMy7kd8_ZLfTz4.webp&quot; alt=&quot;The Magic Mapper home screen on the TV, listing each remote button with its current mapping and status.&quot;&gt;&lt;figcaption&gt;The Magic Mapper home screen on the TV, listing each remote button with its current mapping and status.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;The interface is deliberately television-shaped. There are no tiny form controls, browser-like sidebars, or seventeen equally weighted actions in one modal. Common actions come first; picture and screen controls, devices and automation, experimental commands, and global settings sit one level deeper. Back moves up one level (which turned into the hardest bug in the project).&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/action-catalog.D5LhLlek_Z2oaMlj.webp&quot; alt=&quot;The action catalogue, with upstream actions grouped into remote-friendly categories.&quot;&gt;&lt;figcaption&gt;The action catalogue, with upstream actions grouped into remote-friendly categories.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;back-did-not-mean-back&quot;&gt;Back did not mean Back&lt;a class=&quot;heading-anchor&quot; href=&quot;#back-did-not-mean-back&quot; aria-label=&quot;Permalink to Back did not mean Back&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;With Magic Mapper holding the input device, pressing Back inside another application’s nested menu exited the whole application instead of closing the menu. The UI was not mishandling navigation; every application on the television changed behaviour while the mapper was running.&lt;/p&gt;
&lt;p&gt;Once again, the model went back to work, and together we reduced the problem to the two webOS output devices. Replaying the remote’s raw Back sequence through the existing &lt;code&gt;[2]&lt;/code&gt; passthrough caused an app exit. Sending a clean, complete Back keypress through &lt;code&gt;[1]&lt;/code&gt; closed only the current menu, even while the physical input was exclusively grabbed.&lt;/p&gt;
&lt;p&gt;The fix suppresses the original Back sequence on webOS 25 and replays one fresh keypress through the correct device. We tested it on the actual C3: Back closed only the nested menu, arrow and OK responded immediately afterward, and Netflix and Rakuten remained blocked.&lt;/p&gt;
&lt;p&gt;That change belonged in the mapper rather than the new interface, so I separated it into a one-file &lt;a href=&quot;https://github.com/andrewfraley/magic_mapper/pull/37&quot;&gt;upstream pull request&lt;/a&gt;. The standalone app keeps its UI, packaging, validation, and managed lifecycle separate from the clean upstream fork.&lt;/p&gt;
&lt;p&gt;The frontend eventually moved to Vite, while giving SolidJS 2’s release candidate a try. But the cool part is not the stack; it is how easy it became to bundle everything webOS needs and ship it to the TV.&lt;/p&gt;
&lt;h2 id=&quot;the-volume-bar-that-would-not-go-away&quot;&gt;The volume bar that would not go away&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-volume-bar-that-would-not-go-away&quot; aria-label=&quot;Permalink to The volume bar that would not go away&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Then came the final issue: pressing volume up left the bar on screen until I pressed something else. My first guess was the mic button, because I had disabled it and disabling buttons might well interfere with other buttons’ behaviour. A quick test proved that assumption wrong. What settled it was capturing what webOS actually received:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt; 95.617  EV_KEY  code=115  value=1   # volume up, pressed&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;104.557  EV_KEY  code=115  value=0   # released, nine seconds later&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;104.707  EV_KEY  code=103  value=1   # the D-pad press that flushed it&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key-up for volume arrived &lt;strong&gt;nine seconds&lt;/strong&gt; after the key-down, at the exact moment I pressed the D-pad. The television was not failing to dismiss the bar. It believed the button was still being held.&lt;/p&gt;
&lt;p&gt;The mapper opened the remote with a buffered reader and watched it with &lt;code&gt;select()&lt;/code&gt;, which reports readability from the kernel queue. An evdev device writes a key and its &lt;code&gt;SYN_REPORT&lt;/code&gt; together, so the buffered reader pulled both into user space in a single syscall, handed back one, and left &lt;code&gt;select()&lt;/code&gt; looking at an empty kernel queue. Every event after that arrived one behind. The D-pad never dismissed anything; it was just flushing the release that had been sitting in the buffer.&lt;/p&gt;
&lt;p&gt;The fix was simply setting &lt;code&gt;buffering=0&lt;/code&gt;. The bug was in the first commit of the wrapper, so every release before 1.1.1 carried it, and it affected every button. Volume is just where a stuck key-up is visible.&lt;/p&gt;
&lt;p&gt;Version 1.1.1 is &lt;a href=&quot;https://github.com/afonsojramos/magic-mapper-webos/releases/latest&quot;&gt;available on GitHub&lt;/a&gt; and installable through Homebrew Channel by adding &lt;code&gt;https://mm.afonsojramos.me&lt;/code&gt; as a repository. I also submitted it to the &lt;a href=&quot;https://github.com/webosbrew/apps-repo/pull/232&quot;&gt;webOS Homebrew repository&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-route-i-would-have-taken&quot;&gt;The route I would have taken&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-route-i-would-have-taken&quot; aria-label=&quot;Permalink to The route I would have taken&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Would I have gone through this rabbit hole without AI? Maybe I would have dipped my toes, but time seems to run out more and more with age, the list of things I am involved in only grows, and the depth I reached here would have been &lt;em&gt;very&lt;/em&gt; different.&lt;/p&gt;
&lt;p&gt;Without a model to argue with, I would 100% not have rooted the television. I would have done the cheap thing and never connected it. The Xiaomi box was already attached to the panel and already doing the work. A C3 that never sees a network shows no promotions, sends no telemetry, and has no button that installs Rakuten, because there is nothing for it to install from. That route costs one decision and no evenings.&lt;/p&gt;
&lt;p&gt;What changed is not that I became more capable. It is just that the interesting route stopped costing weeks. Reading SlopBro’s source, checking the recorded consent flags against webOS’s own settings service, working out why an old QML screensaver cannot load against a Flutter implementation, building a webOS application in a stack I had not used: each of those was previously a whole evening, or several, and most of them would have lost to whatever else the week wanted. Compressed, they became things I could attempt on a Tuesday and abandon on Wednesday if the evidence said stop.&lt;/p&gt;
&lt;p&gt;And it is not like the model made the decisions. What to try, what to measure, and when to stop stayed mine; what the model removed was the cost of acting on them. AI is great at enabling curiosity, the same way it is great at enabling a quick MVP.&lt;/p&gt;
&lt;p&gt;None of this means I fully trust it to do everything. A model tends to run with whatever framing you hand it, and I handed it wrong framings the whole way through: the box is underpowered, the mic button broke the volume bar. What kept those errors cheap was insisting on measurements before conclusions: the latency test, the event capture. Agreement is a model’s default state, not evidence.&lt;/p&gt;
&lt;p&gt;There is a cost to this though, one that arrives later and that not everyone is willing to take. AI made the app cheap to build, not cheap to own. Magic Mapper for webOS can now run on other people’s TVs, where I cannot watch it fail, and its first release shipped a bug I lived with for two weeks, the way I treat most small annoyances: ignored until it bugged me enough to dig in. No speed of building would have caught it. And if you check my other open-source projects, you will see that I tend to take care of things and make sure that issues are quickly resolved, but I’m not sure I can say the same about everyone’s AI projects, and that can get annoying and pollute the open-source scene a bit. But when that is the case, a small fork will surely emerge from someone more willing to maintain something that helps others, because that is what keeps open source alive.&lt;/p&gt;
&lt;h2 id=&quot;steering-into-the-rabbit-hole&quot;&gt;Steering into the rabbit hole&lt;a class=&quot;heading-anchor&quot; href=&quot;#steering-into-the-rabbit-hole&quot; aria-label=&quot;Permalink to Steering into the rabbit hole&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I did not fall into this rabbit hole. I steered into it, one question at a time, and most of those turns were possible because root made the hard-to-read parts readable: consent flags, input events, failures. All I needed to do was ask.&lt;/p&gt;
&lt;p&gt;The application that came out of this was not even a goal, it was just a conclusion from a series of questions that derived from the one the Xiaomi planted: if I could remove the things I did not want from Android, how much of the LG experience could I make mine? I was not looking for an open-source project to build. The exploration simply exposed gaps in what I wanted my experience to be, and given that similar projects existed for Android TV, they were not super hard to conceptualize. And if what I made makes other people’s lives easier, all the better!&lt;/p&gt;
&lt;p&gt;AI makes attempts like these cheap. The steering does stay expensive though, because you still need to know the right question to ask. But give in to the curiosity, read more about stuff, maybe even let AI help you through the journey, and at some point you will know what to ask. Because, at the end of the day, you can just do things!&lt;/p&gt;
</content:encoded></item><item><title>Yet Another Package Manager</title><link>https://afonsojramos.me/blog/yet-another-package-manager/</link><guid isPermaLink="true">https://afonsojramos.me/blog/yet-another-package-manager/</guid><description>I was fully prepared to dismiss nub, but its global store really piqued my interest and I ended up with commits in the thing so that my repos fully support it.</description><pubDate>Fri, 17 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When &lt;a href=&quot;https://nubjs.com&quot;&gt;nub&lt;/a&gt; showed up on my radar, my first reaction was the &lt;a href=&quot;https://xkcd.com/927/&quot;&gt;xkcd standards comic&lt;/a&gt;. I already had a working split: bun for the personal sites, pnpm for the serious apps, mise pinning versions across machines. The Node ecosystem does not have a package manager shortage. It has a package manager surplus with a discovery problem.&lt;/p&gt;
&lt;p&gt;What got me to actually try it was the combination of an ambitious filesystem model and a migration story that barely qualified as a migration. nub is a single Rust binary with its own install engine, but it does not require a nub-specific lockfile. It &lt;a href=&quot;https://nubjs.com/docs/install#compatibility&quot;&gt;infers the incumbent package manager and mirrors it&lt;/a&gt;: npm, pnpm, and Bun lockfiles round-trip in their existing formats, while Yarn lockfiles are read-only. Point it at a &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; or &lt;code&gt;bun.lock&lt;/code&gt; and it keeps that format; if the resolved graph has not changed, it leaves the lockfile untouched. Your teammates do not have to switch just because you tried nub locally. That is a very different pitch from “regenerate your lockfile and pray”, and it lowered the cost of an experiment to roughly zero.&lt;/p&gt;
&lt;p&gt;It also runs TypeScript directly (&lt;code&gt;nub file.ts&lt;/code&gt;), runs package scripts, and replaces &lt;code&gt;npx&lt;/code&gt;, and in my case &lt;code&gt;bunx&lt;/code&gt;, with its own &lt;code&gt;nubx&lt;/code&gt;. Nub still runs the code on stock Node, but one command surface replaced most of the separate tools I had accumulated around it.&lt;/p&gt;
&lt;nav class=&quot;table-of-contents&quot;&gt;&lt;ol&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-oxc-signal&quot; title=&quot;The Oxc signal&quot;&gt;The Oxc signal&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-global-store&quot; title=&quot;The global store&quot;&gt;The global store&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#phantom-dependencies-or-my-repos-were-lying-to-me&quot; title=&quot;Phantom dependencies, or: my repos were lying to me&quot;&gt;Phantom dependencies, or: my repos were lying to me&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#install-scripts-are-permissions&quot; title=&quot;Install scripts are permissions&quot;&gt;Install scripts are permissions&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-sharp-edges&quot; title=&quot;The sharp edges&quot;&gt;The sharp edges&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#from-bug-reports-to-commits&quot; title=&quot;From bug reports to commits&quot;&gt;From bug reports to commits&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#verdict&quot; title=&quot;Verdict&quot;&gt;Verdict&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/nav&gt;&lt;h2 id=&quot;the-oxc-signal&quot;&gt;The Oxc signal&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-oxc-signal&quot; aria-label=&quot;Permalink to The Oxc signal&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I have developed a small bias towards projects that use or build on &lt;a href=&quot;https://oxc.rs/&quot;&gt;Oxc&lt;/a&gt;. Not because Rust automatically makes a JavaScript tool good, but because those projects often share priorities I like: native performance, compatibility with the existing ecosystem, and focused components that can be embedded without asking you to move into an entirely new world.&lt;/p&gt;
&lt;p&gt;Nub uses Oxc for a clear reason. Its TypeScript support does not come from replacing Node with another runtime. It &lt;a href=&quot;https://nubjs.com/docs/runtime/typescript&quot;&gt;transpiles the source in memory using Oxc&lt;/a&gt;, through a native addon, and then gives the result to the stock Node binary. That lets it support TypeScript, JSX, decorators, and newer syntax across the Node versions it targets while keeping Node’s runtime compatibility. Oxc handles the part it is good at; Node remains Node.&lt;/p&gt;
&lt;p&gt;I have never seen someone so obsessed with performance, and I love it. When &lt;a href=&quot;https://github.com/yuku-toolchain/yuku&quot;&gt;Yuku&lt;/a&gt; published results faster than Oxc, Boshen did not dismiss the comparison. He praised its data-oriented design, said there was something for Oxc to learn, and immediately started considering the breaking changes it might take to catch up.&lt;/p&gt;
&lt;figure&gt;&lt;a href=&quot;https://x.com/boshen_c/status/2076523616876011666&quot;&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/boshen-yuku-performance.DQH55n_v_ZWp3S6.webp&quot; alt=&quot;A post by Boshen praising Yuku for outperforming Oxc and saying he would consider major breaking changes to reach the same level of performance.&quot;&gt;&lt;/a&gt;&lt;figcaption&gt;A post by Boshen praising Yuku for outperforming Oxc and saying he would consider major breaking changes to reach the same level of performance.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Credit here belongs to Oxc project lead &lt;a href=&quot;https://github.com/Boshen&quot;&gt;Boshen&lt;/a&gt; and the team around him. Building fast infrastructure is one thing. Building it as a set of useful parts that other tools can adopt without inheriting an entire platform is much harder, and I keep finding that I like the projects that make that choice.&lt;/p&gt;
&lt;p&gt;So I migrated one repo. Then ten.&lt;/p&gt;
&lt;h2 id=&quot;the-global-store&quot;&gt;The global store&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-global-store&quot; aria-label=&quot;Permalink to The global store&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The feature that actually sold me is the global virtual store. That name is easy to misunderstand, because it is not merely a download cache.&lt;/p&gt;
&lt;p&gt;Most modern package managers, pnpm included, already have a global content-addressed store. Identical package files are saved once and then hard-linked, reflinked, or copied into projects. The project normally still gets its own &lt;em&gt;virtual store&lt;/em&gt;, though: the fully wired directory tree that encodes which exact dependency tree each package is allowed to see. Rebuilding that tree is why deleting &lt;code&gt;node_modules&lt;/code&gt; can still mean creating thousands of filesystem entries even when every byte is already cached locally.&lt;/p&gt;
&lt;p&gt;Nub, through its embedded aube engine, moves that virtual store into a machine-wide cache too. A project’s &lt;code&gt;node_modules&lt;/code&gt; becomes mostly symlinks into package trees that have already been materialised elsewhere. Those trees are not keyed by package name and version alone. Their identity includes the recursively resolved dependency graph, patches and, when lifecycle builds are involved, the operating system, architecture, and Node version. Two projects share a package directory only when its surrounding graph and build context make that safe. The source files underneath remain content-addressed, so separate graph variants do not necessarily mean separate copies of every byte.&lt;/p&gt;
&lt;p&gt;This part was not invented by Nub. &lt;a href=&quot;https://pnpm.io/settings#enableglobalvirtualstore&quot;&gt;pnpm has an optional global virtual store&lt;/a&gt;, and &lt;a href=&quot;https://github.com/nubjs/nub/blob/v0.4.12/vendor/aube/crates/aube-lockfile/src/graph_hash.rs&quot;&gt;the aube graph hasher shipped inside Nub explicitly describes itself as a port of pnpm’s implementation&lt;/a&gt;. The difference that mattered to me is how Nub deploys the idea. pnpm leaves the global virtual store disabled for normal project installs. Nub enables it by default on a developer machine, regardless of whether the repository belongs to Bun, pnpm, npm, or Nub itself. Both tools disable the shared layout in CI, where an external machine-wide store would make the resulting tree non-portable.&lt;/p&gt;
&lt;p&gt;Making that the default is the interesting work. A package inside a machine-global store can no longer walk upward and find undeclared dependencies or project files. pnpm’s global virtual store handles hoisted dependencies through &lt;code&gt;NODE_PATH&lt;/code&gt;, but Node does not use &lt;code&gt;NODE_PATH&lt;/code&gt; for ESM; pnpm therefore points affected projects towards &lt;code&gt;packageExtensions&lt;/code&gt; or a custom ESM loader. Nub takes a more targeted route. Nub 0.4.12 &lt;a href=&quot;https://github.com/nubjs/nub/blob/v0.4.12/crates/nub-cli/src/dynamic_phantom.rs&quot;&gt;scans installed package code for those undeclared imports&lt;/a&gt; and materialises only the affected package closure back inside the project. The rest of the graph remains shared.&lt;/p&gt;
&lt;p&gt;The same problem appears when a dependency mutates its own installation. Prisma, for example, writes a generated client beside &lt;code&gt;@prisma/client&lt;/code&gt;. A globally shared directory cannot safely hold generated clients for projects with different schemas, so Nub detects packages with this behaviour and keeps them project-local. This is the recurring design: share the common case globally, then identify the pieces whose correctness depends on project-local state and pull only those pieces back.&lt;/p&gt;
&lt;p&gt;Some tools need a wider escape hatch. Next.js and Metro-based React Native projects resolve modules by crawling within the project and cannot see packages whose real paths live in a machine-wide store, so Nub falls back to a project-local virtual store for them. Expo releases before SDK 56 follow the same path, while newer versions can retain the shared store. &lt;a href=&quot;https://github.com/nubjs/nub/blob/v0.4.12/crates/nub-cli/src/pm_engine/vite_compat.rs&quot;&gt;Vite gets its own compatibility path&lt;/a&gt;: Nub writes the external store into &lt;code&gt;node_modules/.modules.yaml&lt;/code&gt;, which Vite 8.1 and later understand directly, and backports the same check into older installed versions. It preserves global sharing without requiring a &lt;code&gt;vite.config&lt;/code&gt; change or a Nub-owned runtime process.&lt;/p&gt;
&lt;p&gt;This is a much more interesting distinction than dependency isolation alone. pnpm already has an isolated linker, although its default layout deliberately hoists dependencies into a hidden directory for ecosystem compatibility. Nub’s default is stricter: undeclared imports fail unless its compatibility analysis finds a reason to materialise that package locally. Its underlying graph-addressed store owes a great deal to pnpm, but Nub’s bet is that a global virtual store can be the normal local-development path if the package manager is willing to detect and contain the exceptions automatically. That extra machinery is also where several of the sharp edges I found came from.&lt;/p&gt;
&lt;p&gt;After migrating everything, my machine-wide store sits at 7.4 GB total while per-project &lt;code&gt;node_modules&lt;/code&gt; directories dropped to roughly 100-400 MB of residuals. A cold install of a 782-package app takes about 16 seconds; a warm one takes six. And &lt;code&gt;rm -rf node_modules&lt;/code&gt; stops being an event, because there is almost nothing in there.&lt;/p&gt;
&lt;h2 id=&quot;phantom-dependencies-or-my-repos-were-lying-to-me&quot;&gt;Phantom dependencies, or: my repos were lying to me&lt;a class=&quot;heading-anchor&quot; href=&quot;#phantom-dependencies-or-my-repos-were-lying-to-me&quot; aria-label=&quot;Permalink to Phantom dependencies, or: my repos were lying to me&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The stricter layout comes with a stricter philosophy: if you did not declare a dependency, you do not get to import it. I expected this to be an annoyance. Instead it was an audit I did not know I needed, because the migration immediately surfaced bugs in &lt;em&gt;my&lt;/em&gt; code that bun’s flat &lt;code&gt;node_modules&lt;/code&gt; had been hiding for months:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One site referenced &lt;code&gt;@cloudflare/workers-types&lt;/code&gt; in a triple-slash directive without ever declaring it, through a path that had also been dead since v5 of the package. It type-checked by pure hoisting luck.&lt;/li&gt;
&lt;li&gt;Another app’s SSO test suite imported &lt;code&gt;samlify&lt;/code&gt; undeclared. Under the flat layout those tests silently resolved it; under nub they failed to load, and it turned out three test suites had effectively never run. Declaring one devDependency took the suite from 256 to 259 tests.&lt;/li&gt;
&lt;li&gt;This very site threw &lt;code&gt;SessionStorageInitError&lt;/code&gt; in production because Astro’s Cloudflare session driver dynamically imports &lt;code&gt;unstorage&lt;/code&gt;, which only existed transitively. The documented fix is to declare it. The flat layout had simply been papering over it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these were nub bugs. They were my bugs, and nobody had told me about them.&lt;/p&gt;
&lt;h2 id=&quot;install-scripts-are-permissions&quot;&gt;Install scripts are permissions&lt;a class=&quot;heading-anchor&quot; href=&quot;#install-scripts-are-permissions&quot; aria-label=&quot;Permalink to Install scripts are permissions&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://nubjs.com/docs/install#lifecycle-scripts&quot;&gt;install policy&lt;/a&gt; follows the same pattern. Dependency lifecycle scripts do not run indiscriminately: packages must be explicitly approved or pass Nub’s curated default-trust floor, which combines registry provenance, advisory vetting, and a 24-hour cooling window. The permission is written using the incumbent package manager’s own configuration—&lt;code&gt;pnpm.onlyBuiltDependencies&lt;/code&gt;, Bun’s &lt;code&gt;trustedDependencies&lt;/code&gt;, or Nub’s neutral &lt;code&gt;allowBuilds&lt;/code&gt;—rather than introducing a Nub-only decision that collaborators cannot see.&lt;/p&gt;
&lt;p&gt;This was not what made me try Nub, and modern pnpm has also moved towards safer supply-chain defaults. What I like is the combination: Nub preserves the repository’s existing package-manager identity while applying a deliberately conservative install policy underneath it.&lt;/p&gt;
&lt;h2 id=&quot;the-sharp-edges&quot;&gt;The sharp edges&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-sharp-edges&quot; aria-label=&quot;Permalink to The sharp edges&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;It was not all smooth, and this is where it got fun. Migrating ten real repos in a weekend is a decent stress test, and it shook loose a series of genuine nub bugs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The global store’s isolation broke TypeScript’s peer-type resolution in three different disguises: astro-icon’s &lt;code&gt;&amp;lt;Icon&amp;gt;&lt;/code&gt; losing its prop types, TanStack Start inference collapsing into 30-something &lt;code&gt;implicit any&lt;/code&gt; errors, and &lt;code&gt;@react-pdf/renderer&lt;/code&gt; components refusing to be JSX. The root cause, dug out with &lt;code&gt;tsc --traceResolution&lt;/code&gt;, is that a package’s realpath escapes into the global store, so the type-checker’s upward walk never finds the project’s &lt;code&gt;@types&lt;/code&gt; (&lt;a href=&quot;https://github.com/nubjs/nub/issues/450&quot;&gt;#450&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nub import&lt;/code&gt; converted bun lockfiles without running peer resolution, producing lockfiles the install path assumed were already peer-resolved. Result: &lt;code&gt;Cannot find package &apos;vite&apos;&lt;/code&gt; at runtime (&lt;a href=&quot;https://github.com/nubjs/nub/issues/453&quot;&gt;#453&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Playwright’s default webServer teardown is SIGKILL, which no userspace signal forwarding can catch, so &lt;code&gt;nub run&lt;/code&gt; wrappers orphaned dev servers and pinned a CI job for two hours (&lt;a href=&quot;https://github.com/nubjs/nub/issues/463&quot;&gt;#463&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Cloudflare’s build image had no way to provision nub at all (&lt;a href=&quot;https://github.com/nubjs/nub/issues/454&quot;&gt;#454&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;from-bug-reports-to-commits&quot;&gt;From bug reports to commits&lt;a class=&quot;heading-anchor&quot; href=&quot;#from-bug-reports-to-commits&quot; aria-label=&quot;Permalink to From bug reports to commits&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I did not treat these issues as simple &lt;em&gt;“found this, fix pls”&lt;/em&gt; reports. They were excuses to dive deeper into &lt;code&gt;nub&lt;/code&gt;’s codebase, and they led to three PRs. &lt;a href=&quot;https://github.com/nubjs/nub/pull/452&quot;&gt;#452&lt;/a&gt; taught the phantom detector to see type-only imports inside Astro/Vue/Svelte components; the maintainer stacked two commits on top of my branch extending it to &lt;code&gt;.d.ts&lt;/code&gt; surfaces and merged the lot. &lt;a href=&quot;https://github.com/nubjs/nub/pull/464&quot;&gt;#464&lt;/a&gt; arms &lt;code&gt;PR_SET_PDEATHSIG&lt;/code&gt; on the script child so a SIGKILL on nub can no longer orphan the workload; merged untouched, kill-9 regression test and all. Both shipped in &lt;a href=&quot;https://github.com/nubjs/nub/releases/tag/v0.4.12&quot;&gt;v0.4.12&lt;/a&gt; within a day, at which point I got to delete every &lt;code&gt;node-linker=hoisted&lt;/code&gt; workaround I had scattered across the fleet and verify zero type errors under full isolation. A third PR is &lt;a href=&quot;https://github.com/nubjs/nub/pull/458&quot;&gt;still open&lt;/a&gt;, covering a related edge case. Nub maintainer &lt;a href=&quot;https://github.com/colinhacks&quot;&gt;Colin McDonnell&lt;/a&gt; and I happened to arrive at similar fixes independently and at roughly the same time.&lt;/p&gt;
&lt;p&gt;For the Cloudflare gap I built &lt;a href=&quot;https://github.com/afonsojramos/asdf-nub&quot;&gt;asdf-nub&lt;/a&gt;, an asdf/mise plugin that installs the official release binaries. It has since been &lt;a href=&quot;https://github.com/asdf-vm/asdf-plugins/pull/1170&quot;&gt;adopted into the nubjs org&lt;/a&gt;, and there is a &lt;a href=&quot;https://github.com/cloudflare/pages-build-image/issues/13&quot;&gt;request open with Cloudflare&lt;/a&gt; to support it natively.&lt;/p&gt;
&lt;h2 id=&quot;verdict&quot;&gt;Verdict&lt;a class=&quot;heading-anchor&quot; href=&quot;#verdict&quot; aria-label=&quot;Permalink to Verdict&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Nub did not invent dependency isolation or the graph-addressed global virtual store. What sold me was its opinionated version of the idea: make machine-wide reuse the normal local path, build compatibility machinery around the places where that breaks, and do it while preserving the lockfiles already spread across my Bun and pnpm projects. I could use the filesystem model I wanted without first standardising every repository on the same package manager or asking collaborators to change their workflow. The stricter layout then exposed real mistakes in my projects, tests, and this site, but that was a useful consequence rather than the reason to switch.&lt;/p&gt;
&lt;p&gt;I have gone far enough that the other package-manager commands in my shell are now aliases to &lt;code&gt;nub&lt;/code&gt;. The repositories still keep their Bun or pnpm lockfiles; my muscle memory just no longer gets to choose the installer.&lt;/p&gt;
&lt;p&gt;Yet another package manager. This one earned it.&lt;/p&gt;
</content:encoded></item><item><title>Quality Is How You Move Fast</title><link>https://afonsojramos.me/blog/quality-is-how-you-move-fast/</link><guid isPermaLink="true">https://afonsojramos.me/blog/quality-is-how-you-move-fast/</guid><description>LLMs did not create software&apos;s obsession with speed. They made it cheap enough to expose what we were already willing to sacrifice for it.</description><pubDate>Thu, 16 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The recent dispute between Bun creator Jarred Sumner and Zig creator Andrew Kelley made me uncomfortable enough that I went back and &lt;a href=&quot;/blog/elysia-vs-hono-astro-cloudflare&quot;&gt;updated an old post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I had ended that post by saying I was “extremely bullish” on Bun. I wrote that its built-in server might eventually make us reach for frameworks like Hono and Elysia less often. I still use Bun in production, and much of what impressed me then still impresses me now. But I have also dealt with memory leaks in long-running Bun processes, along with surprise Railway bills. I upgraded Bun immediately, but the public discussion around its rewrite from Zig to Rust made me less willing to treat those incidents as version-specific bugs that were safely behind me.&lt;/p&gt;
&lt;p&gt;Bun’s &lt;a href=&quot;https://bun.com/blog/bun-in-rust&quot;&gt;account of the rewrite&lt;/a&gt; is unusually candid about the memory leaks, use-after-free bugs, crashes, and other stability problems that motivated it. Kelley responded with &lt;a href=&quot;https://andrewkelley.me/post/my-thoughts-bun-rust-rewrite.html&quot;&gt;a much harsher explanation&lt;/a&gt;: the problem was not Zig, he argued, but a culture that accumulated technical debt while racing from feature to feature. The exchange became personal, and I have no interest in refereeing the history between them. Both are far better systems programmers than I am.&lt;/p&gt;
&lt;p&gt;What unsettled me was the engineering philosophy underneath the argument.&lt;/p&gt;
&lt;nav class=&quot;table-of-contents&quot;&gt;&lt;ol&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#ai-made-an-old-trade-off-cheaper&quot; title=&quot;AI made an old trade-off cheaper&quot;&gt;AI made an old trade-off cheaper&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#quality-is-not-the-opposite-of-speed&quot; title=&quot;Quality is not the opposite of speed&quot;&gt;Quality is not the opposite of speed&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-smallest-useful-habit&quot; title=&quot;The smallest useful habit&quot;&gt;The smallest useful habit&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/nav&gt;&lt;h2 id=&quot;ai-made-an-old-trade-off-cheaper&quot;&gt;AI made an old trade-off cheaper&lt;a class=&quot;heading-anchor&quot; href=&quot;#ai-made-an-old-trade-off-cheaper&quot; aria-label=&quot;Permalink to AI made an old trade-off cheaper&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The software industry’s fixation on speed did not arrive with LLMs. “Move fast and break things” predates ChatGPT by more than a decade. Startups have always raced to launch, disrupt a market, copy a competitor, or reach feature parity before the runway runs out.&lt;/p&gt;
&lt;p&gt;LLMs did not create that instinct. They removed a great deal of the friction that used to constrain it.&lt;/p&gt;
&lt;p&gt;If code is cheap to generate, why spend time making it maintainable? If an agent can rewrite the implementation tomorrow, why understand the one it produced today? If the test suite passes, why read a million-line diff?&lt;/p&gt;
&lt;p&gt;Those questions sound new because the scale is new, but the values behind them are not. We have always had teams that treated code as disposable output and teams that treated it as a system somebody would have to understand later. Coding agents simply let both groups move faster in the direction they were already heading.&lt;/p&gt;
&lt;p&gt;That is why the Bun rewrite is such a useful case study. Sumner describes using around 50 Claude workflows over 11 days to translate more than half a million lines of Zig into Rust.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/bun-rust-migration-commits.DYdTWull_Z18q9Ig.webp&quot; alt=&quot;A heatmap of 6,502 commits made over the 11-day Bun Rust migration, with a peak of 695 commits in one hour.&quot;&gt;&lt;figcaption&gt;A heatmap of 6,502 commits made over the 11-day Bun Rust migration, with a peak of 695 commits in one hour.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;This was not a one-line “rewrite Bun” prompt: before the translation, Sumner worked with Claude to produce a detailed &lt;a href=&quot;https://github.com/oven-sh/bun/blob/3157cb14b5970b69532a47800504a28ef5963e22/docs/PORTING.md&quot;&gt;Zig-to-Rust porting guide&lt;/a&gt; that mapped types, lifetimes, naming conventions, and common idioms for the agents to follow. It is an extraordinary demonstration of what coding agents can do. It is also a million-line change that no human could review in the conventional sense. Bun instead relied on its test suite and adversarial review agents to establish confidence in the result.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/bun-rust-migration-workflow.DVLmaXSK_ZTeTIc.webp&quot; alt=&quot;A replay of Bun’s Claude Code migration workflow, showing 1,610 fix commits divided among 64 Claude agents across four worktrees.&quot;&gt;&lt;figcaption&gt;A replay of Bun’s Claude Code migration workflow, showing 1,610 fix commits divided among 64 Claude agents across four worktrees.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Maybe that works. I genuinely hope it does. But passing tests and maintainable software are not the same property. Tests can show that the behaviours somebody anticipated still work. They cannot prove that the new implementation is understandable, that its abstractions will survive the next five years, or that the test suite covers the assumptions carried silently from the old code.&lt;/p&gt;
&lt;p&gt;The rewrite changes the language. It does not automatically change the incentives that produced the original codebase. That does not mean Bun’s Rust codebase is necessarily bad. The porting guide, test suite, and adversarial reviews may have produced something excellent. It does mean that the primary goal of its creation was a fast, faithful translation, not the slow cultivation of an idiomatic Rust codebase that humans had reviewed line by line. That trade-off is acceptable for plenty of software. I am less certain about it in a runtime that may become the foundation underneath everything else an application does.&lt;/p&gt;
&lt;p&gt;Bun is not alone in testing that boundary. Node.js core maintainer Matteo Collina opened a &lt;a href=&quot;https://github.com/nodejs/node/pull/61478&quot;&gt;large, AI-assisted virtual file system pull request&lt;/a&gt;, disclosing that he had used a significant amount of Claude Code and personally reviewed every change. The implementation grew to around 9,200 lines, alongside more than 11,000 lines of tests, and the resulting debate caught even one of Node’s most experienced maintainers in a much larger argument about authorship, reviewability, and responsibility. Former Node core contributor Fedor Indutny responded with &lt;a href=&quot;https://github.com/indutny/no-ai-in-nodejs-core&quot;&gt;a petition to reject LLM-generated pull requests from Node core&lt;/a&gt;. The comparison makes the question harder, not easier: Node has mature governance and an experienced maintainer explicitly accepting responsibility for the code, yet the scale of AI-assisted work still strained the community’s idea of what meaningful review looks like.&lt;/p&gt;
&lt;p&gt;And AI is not required for review and process to fail. Collina recently described triaging a Node.js vulnerability, writing the fix, and pushing it through the security process, only to conclude later that it should not have been a CVE at all—and that the fix had broken a large part of the ecosystem.&lt;/p&gt;
&lt;figure&gt;&lt;a href=&quot;https://x.com/matteocollina/status/2072712929179324658&quot;&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/matteo-collina-node-cve.TJrErw3c_ZqNX0i.webp&quot; alt=&quot;A post by Matteo Collina explaining that he triaged and fixed a Node.js vulnerability, but later concluded it should not have been a CVE and that the fix broke much of the ecosystem.&quot;&gt;&lt;/a&gt;&lt;figcaption&gt;A post by Matteo Collina explaining that he triaged and fixed a Node.js vulnerability, but later concluded it should not have been a CVE and that the fix broke much of the ecosystem.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Michael Arnaldi highlighted two uncomfortable details from the aftermath:&lt;/p&gt;
&lt;figure&gt;&lt;a href=&quot;https://x.com/MichaelArnaldi/status/2076326793343070556&quot;&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/michael-arnaldi-skill-over-time.BHMbAss-_Z1cYJBL.webp&quot; alt=&quot;A post by Michael Arnaldi with a graph arguing that AI increases the gap in skill between top and mid-level developers.&quot;&gt;&lt;/a&gt;&lt;figcaption&gt;A post by Michael Arnaldi with a graph arguing that AI increases the gap in skill between top and mid-level developers.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;figure&gt;&lt;a href=&quot;https://x.com/MichaelArnaldi/status/2076328507265646930&quot;&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/michael-arnaldi-confidence-over-time.Caz2o83l_ZsmhJG.webp&quot; alt=&quot;A post by Michael Arnaldi with a graph arguing that AI can increase mid-level developers’ confidence even as their relative skill falls.&quot;&gt;&lt;/a&gt;&lt;figcaption&gt;A post by Michael Arnaldi with a graph arguing that AI can increase mid-level developers’ confidence even as their relative skill falls.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;The point is not that Collina is careless. It is almost the opposite: experience, tests, and an established security process still did not make the consequences obvious before the change shipped. AI increases the volume of decisions we can make, but it does not proportionally increase our ability to understand their consequences. A green test suite cannot be the whole definition of quality, because both humans and agents can optimize the evidence while missing what the software will do in the wider ecosystem.&lt;/p&gt;
&lt;h2 id=&quot;quality-is-not-the-opposite-of-speed&quot;&gt;Quality is not the opposite of speed&lt;a class=&quot;heading-anchor&quot; href=&quot;#quality-is-not-the-opposite-of-speed&quot; aria-label=&quot;Permalink to Quality is not the opposite of speed&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;We often talk about quality and speed as opposite ends of a slider. Move one up and the other must come down. That framing is convenient because it makes every shortcut sound like an explicit business decision: yes, we know this is messy, but right now we need velocity.&lt;/p&gt;
&lt;p&gt;In my experience, the trade-off only works that cleanly over very short periods.&lt;/p&gt;
&lt;p&gt;You can move quickly through an empty codebase. You can also move quickly through a codebase whose boundaries are clear, whose behaviour is tested, and whose previous authors left enough context for the next person. What slows a team down is the middle state: software that ships rapidly but makes every subsequent change more uncertain.&lt;/p&gt;
&lt;p&gt;The pull request that skips an edge case saves an hour today. The missing test costs a day when somebody changes the same path six months later. The abstraction nobody understood was faster to generate than to design, until five features depend on it and every modification requires another workaround.&lt;/p&gt;
&lt;p&gt;This is why the engineers who appear most preoccupied with quality are often the ones moving fastest over a meaningful span of time. They review the pull request. They ask what happens on the strange input. They remove ambiguity before it spreads. None of that looks fast while it is happening, because the time saved belongs to the future and is difficult to put in a sprint report.&lt;/p&gt;
&lt;p&gt;Today, that scrutiny does not have to begin when a pull request reaches somebody else. It can happen while you build: iterating with an LLM, asking it to challenge the approach, inspecting its diff, and testing the assumptions together before the change leaves your machine. A later review still matters because a fresh context can see what the author and agent normalized along the way, but quality is stronger when it is part of the implementation loop rather than a gate at the end of it.&lt;/p&gt;
&lt;p&gt;Quality, in this context, does not mean clever architecture or adherence to every fashionable best practice. It means maintainability: correct software that can continue changing without requiring its authors to rediscover the entire system each time.&lt;/p&gt;
&lt;h2 id=&quot;the-smallest-useful-habit&quot;&gt;The smallest useful habit&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-smallest-useful-habit&quot; aria-label=&quot;Permalink to The smallest useful habit&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I am not writing this from a position of purity. I have shipped plenty of software I would not want used as evidence of my engineering philosophy. I use coding agents every day, often aggressively. I like that they let me build more than I could before, and I am not interested in returning to an era where typing speed or tolerance for repetitive work determined what got made.&lt;/p&gt;
&lt;p&gt;The lesson I take from all of this is not that generated code is bad, that Rust is better than Zig, or that moving quickly is irresponsible. It is that abundant implementation makes judgment more important, not less.&lt;/p&gt;
&lt;p&gt;When producing another version is nearly free, the scarce work is deciding whether the version is good. Someone still has to understand the trade-offs, inspect the boundaries, question the happy path, and decide what the software should be able to survive. An agent can participate in all of that, but asking it to review its own output twice does not make those decisions disappear.&lt;/p&gt;
&lt;p&gt;Linus Torvalds recently &lt;a href=&quot;https://lore.kernel.org/linux-media/CAHk-=wi4zC+Ze8e+p3tMv8TtG_80KzsZ1syL9anBtmEh5Z40vg@mail.gmail.com/&quot;&gt;made a similar distinction from the maintainer’s side&lt;/a&gt;. Linux is not anti-AI; he considers the tool plainly useful. But its use should help maintainers rather than transfer more work onto them. That feels like the right standard. The question is not whether AI touched the code, but whether somebody understood the result, accepted responsibility for it, and left reviewers with something worth reviewing.&lt;/p&gt;
&lt;p&gt;The smallest useful response is still code review. Read what your agents produce. Read what your colleagues produce. Ask the annoying question about the edge case. And follow the small code smells instead of automatically working around them. If a function name does not quite describe what the function does, find out why. Read the surrounding code, understand the current state of the system, and decide whether the mismatch is local or evidence of an abstraction that has drifted over time.&lt;/p&gt;
&lt;p&gt;Leave things better than you found them. That advice used to come with a real risk: a thirty-minute change could trigger several days of refactoring before you were confident enough to ship it. Coding agents have made that investigative work much cheaper. You can trace callers, rename an unclear concept, update the tests, and verify the surrounding behaviour without immediately disappearing into the kind of refactoring loop that once made this instinct difficult to defend. That does not mean expanding every task into a rewrite. It means we have fewer excuses for preserving confusion when understanding and improving it is now so much faster.&lt;/p&gt;
&lt;p&gt;Software has always rewarded speed, and that pressure has produced plenty of remarkable things. But once you have worked in the same system long enough, the relationship starts to look reversed: quality is not what you sacrifice to move fast. Quality is how you keep moving fast after the first release.&lt;/p&gt;
</content:encoded></item><item><title>Relentless by Default</title><link>https://afonsojramos.me/blog/relentless-by-default/</link><guid isPermaLink="true">https://afonsojramos.me/blog/relentless-by-default/</guid><description>Claude Fable went to absurd lengths to fix a trivial CSS bug. The gap isn&apos;t intelligence, it&apos;s proportion, and that relentlessness is mostly a feature. What actually matters now is who gets to decide which model answers you, and whether anyone tells you when they do.</description><pubDate>Wed, 17 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Simon Willison wrote up &lt;a href=&quot;https://simonwillison.net/2026/Jun/11/fable-is-relentlessly-proactive/&quot;&gt;a debugging session with Claude Fable 5&lt;/a&gt; that’s been rattling around in my head. He noticed a stray horizontal scrollbar in a chat input, took a screenshot, and gave the model one line: “Look at dependencies to help figure out why there is a horizontal scrollbar here.”&lt;/p&gt;
&lt;p&gt;Then he walked away. When he came back, his machine was opening browser windows on its own.&lt;/p&gt;
&lt;p&gt;Fable had built its own way to screenshot browser windows, enumerating every open window on macOS through &lt;code&gt;pyobjc&lt;/code&gt; and grabbing PNGs with the &lt;code&gt;screencapture&lt;/code&gt; CLI. It stood up a small Python web server with permissive CORS headers so a page could POST measurements back to disk. It edited Datasette’s own templates to inject JavaScript that fired the &lt;code&gt;/&lt;/code&gt; keyboard shortcut on page load, just so the modal it needed to inspect would open by itself. Then it reached through a Web Component’s shadow DOM to read the computed styles it was after.&lt;/p&gt;
&lt;p&gt;The fix, in the end, was two lines of CSS. The session cost about $12.&lt;/p&gt;
&lt;p&gt;A comment I saw summed up a reaction I’ve seen a lot: this is evidence none of these models are actually intelligent, because any junior dev would have fixed it faster and with far less ceremony.&lt;/p&gt;
&lt;p&gt;I think it shows the opposite.&lt;/p&gt;
&lt;nav class=&quot;table-of-contents&quot;&gt;&lt;ol&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#looking-at-what-it-actually-did&quot; title=&quot;Looking at what it actually did&quot;&gt;Looking at what it actually did&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#a-ten-cent-job-a-twelve-dollar-answer&quot; title=&quot;A ten-cent job, a twelve-dollar answer&quot;&gt;A ten-cent job, a twelve-dollar answer&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-industry-is-already-building-the-governor&quot; title=&quot;The industry is already building the governor&quot;&gt;The industry is already building the governor&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-3&quot;&gt;&lt;a href=&quot;#cursors-composer-25-the-honest-version&quot; title=&quot;Cursor’s Composer 2.5: the honest version&quot;&gt;Cursor’s Composer 2.5: the honest version&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-3&quot;&gt;&lt;a href=&quot;#gpt-5s-router-the-cautionary-version&quot; title=&quot;GPT-5’s router: the cautionary version&quot;&gt;GPT-5’s router: the cautionary version&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#where-this-should-go-next&quot; title=&quot;Where this should go next&quot;&gt;Where this should go next&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#so-not-a-failure&quot; title=&quot;So, not a failure&quot;&gt;So, not a failure&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/nav&gt;&lt;h2 id=&quot;looking-at-what-it-actually-did&quot;&gt;Looking at what it actually did&lt;a class=&quot;heading-anchor&quot; href=&quot;#looking-at-what-it-actually-did&quot; aria-label=&quot;Permalink to Looking at what it actually did&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;No junior developer would have actually chained that together. And it is not because these individual tricks are black magic. Grabbing screenshots of windows is something &lt;a href=&quot;https://alttab.io/&quot;&gt;AltTab&lt;/a&gt; and &lt;a href=&quot;https://github.com/ejbills/DockDoor&quot;&gt;DockDoor&lt;/a&gt; already do, so the technique is out there. Deriving the whole pipeline from a one-line prompt is the part that isn’t.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/fable-5-intelligence-index.DFfCWoNE_PKfFH.webp&quot; alt=&quot;Artificial Analysis Intelligence Index bar chart with Claude Fable 5 ranked first at 64.9, just ahead of Claude Opus 4.8 at 61.4 and GPT-5.5 at 60.2.&quot;&gt;&lt;figcaption&gt;Artificial Analysis Intelligence Index bar chart with Claude Fable 5 ranked first at 64.9, just ahead of Claude Opus 4.8 at 61.4 and GPT-5.5 at 60.2.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;em&gt;This is not a dim model overreaching. Fable launched at the top of &lt;a href=&quot;https://artificialanalysis.ai/articles/claude-fable-5-mythos-intelligence-index&quot;&gt;Artificial Analysis&lt;/a&gt;’ Intelligence Index, with Opus 4.8 (the model it later falls back to) the closest thing behind it.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Obviously, a senior who needed those computed styles would simply open DevTools. Fable built a web server instead.&lt;/p&gt;
&lt;p&gt;That is not what unintelligent looks like, necessarily, but it is what intelligence with no sense of scale looks like, which is not necessarily a bad thing, if the goal is to get the job done quickly and efficiently. What is more expensive, the model or the human? More often than we admit, the answer is the human, which makes even an overzealous model the cheaper way to get the job done.&lt;/p&gt;
&lt;aside class=&quot;callout callout-note&quot; role=&quot;note&quot;&gt;
&lt;div class=&quot;callout-title&quot;&gt;An uncomfortable aside&lt;/div&gt;
&lt;p&gt;It is worth sitting with what that sentence quietly assumes. The moment a model is reliably cheaper than the person who used to do the work, the question stops being about productivity and starts being about replacement, and that is a far easier trade to celebrate when it is your own time being freed up than when it is your salary on the other side of it. We are optimising hard for the case where the machine wins that comparison, without having really decided, as a society, what we owe the people it wins against.&lt;/p&gt;
&lt;/aside&gt;
&lt;h2 id=&quot;a-ten-cent-job-a-twelve-dollar-answer&quot;&gt;A ten-cent job, a twelve-dollar answer&lt;a class=&quot;heading-anchor&quot; href=&quot;#a-ten-cent-job-a-twelve-dollar-answer&quot; aria-label=&quot;Permalink to A ten-cent job, a twelve-dollar answer&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;So the thing Fable is actually missing isn’t intelligence. It’s proportion, a sense of scale.&lt;/p&gt;
&lt;p&gt;It threw an entire research project at a scrollbar. There’s no internal voice telling it that this is cheap, the stakes are low, so just stop digging. Proactivity cranked to eleven, with no instinct for when to ease off. Simon put it perfectly: Fable will “quite happily burn $12 in tokens inventing new ways to debug your CSS.”&lt;/p&gt;
&lt;p&gt;But, to be fair, it worked. Fable found the bug, tested a fix, and verified it. Expensive and theatrical, sure, but correct.&lt;/p&gt;
&lt;p&gt;And it’s that last part, the checking its own work, that I don’t want to skip past. Early on, you had to babysit these models into doing it. You’d end a prompt with “now write a test and actually run it,” or “are you sure? go back and double-check,” because left to themselves they’d hand you something plausible and call it done. Fable closed that loop on its own, unprompted. That’s a genuine shift in how much you can trust the thing: you can hand it a task, wander off like Simon did, and come back to an answer it has already tried to break itself. The same relentlessness that overshoots on a scrollbar is what makes it check its own work without being told, and honestly, I’ll take that trade most days.&lt;/p&gt;
&lt;p&gt;Of course, the spending cuts both ways. For Anthropic, a model this eager sells more tokens today, and that suits the company doing the billing just fine. For the rest of us, $12 is nothing next to an hour of a senior engineer’s time, so maybe you genuinely don’t care. But a model that can’t tell a ten-cent job from a twelve-dollar one is also exactly what makes people call it dumb in the first place.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/intelligence-vs-price.B7b3d4H4_o0tNe.webp&quot; alt=&quot;Artificial Analysis scatter of intelligence against blended price per million tokens. Claude Fable 5 sits alone on the far right near $7.70, while a green “most attractive quadrant” holds a cluster of nearly-as-capable models, including Claude Opus 4.8 at roughly half the price.&quot;&gt;&lt;figcaption&gt;Artificial Analysis scatter of intelligence against blended price per million tokens. Claude Fable 5 sits alone on the far right near $7.70, while a green “most attractive quadrant” holds a cluster of nearly-as-capable models, including Claude Opus 4.8 at roughly half the price.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;em&gt;The proportion problem, drawn out. Fable is the lone dot stranded on the far right, paying frontier prices, while a whole quadrant of models gets you most of the intelligence for a fraction of the cost. Source: &lt;a href=&quot;https://artificialanalysis.ai/&quot;&gt;Artificial Analysis&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-industry-is-already-building-the-governor&quot;&gt;The industry is already building the governor&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-industry-is-already-building-the-governor&quot; aria-label=&quot;Permalink to The industry is already building the governor&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;And here’s the part I find most interesting. Somewhere in the middle of that session, Fable hit some invisible guardrail and quietly downgraded itself to Opus, a step down from where it started, which then picked up the full transcript and carried on to finish the job. That little switch is the tell. The platform, not me, decided which model was going to answer, did it mid-task, and never really asked. Nobody flipped a setting. It just happened.&lt;/p&gt;
&lt;p&gt;And silent switching like that is exactly what a lot of people are furious about right now. Days after Fable launched, someone dug a paragraph out of its 319-page system card showing that the model will deliberately weaken its own answers when it decides you’re working on cutting-edge AI development, and do it without telling you. Not a refusal, not a visible “I’ve moved you to a smaller model,” just a quietly worse response you’d have no way of knowing was worse. Anthropic reckoned it touched around 0.03% of traffic. It didn’t matter. Fortune wrote it up, critics called it “secret sabotage,” and even researchers who usually defend Anthropic were appalled. Nathan Lambert, fresh from leading open-model work at AI2, said having his access to the frontier model “rug pulled in an under the table fashion” was “appalling,” and that it painted Anthropic as “anti-science.”&lt;/p&gt;
&lt;p&gt;From a pure benchmarking point of view, the anger makes complete sense. If you don’t know which model actually ran, you can’t reproduce a result, you can’t compare two of them, you can’t even tell whether your prompt got worse or the model did. The platform knows exactly what changed; you’re left guessing. And paying for the top tier while quietly being handed something weaker isn’t routing, it’s just being shortchanged.&lt;/p&gt;
&lt;p&gt;But step out of the benchmark mindset and into normal day-to-day usage, and there’s a point hiding underneath all this that I think gets lost. Most of what I actually ask these tools to do doesn’t need the absolute frontier model. If a cheaper one quietly finishes the job just as well, faster and for less money, have I really lost anything? Usually not. The benchmark crowd cares which model ran. The rest of us mostly care that the thing got done. Those are genuinely different requirements, and routing only curdles into a betrayal when it crosses from “right-sized the job” into “gave you less than you paid for, and hid it.” The fix was never to ban the routing. It’s to make it visible, which, to their credit, is roughly what Anthropic ended up doing within a couple of days, apologising that they’d “made the wrong tradeoff.”&lt;/p&gt;
&lt;p&gt;And underneath all the Fable drama, plainer cost-driven routing is coming whether we like it or not. Two recent launches show that version of the idea handled in two very different ways.&lt;/p&gt;
&lt;h3 id=&quot;cursors-composer-25-the-honest-version&quot;&gt;Cursor’s Composer 2.5: the honest version&lt;a class=&quot;heading-anchor&quot; href=&quot;#cursors-composer-25-the-honest-version&quot; aria-label=&quot;Permalink to Cursor’s Composer 2.5: the honest version&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Cursor’s &lt;a href=&quot;https://cursor.com/blog/composer-2-5&quot;&gt;Composer 2.5&lt;/a&gt; is basically a bet on proportion as a product. The whole pitch is speed and cost. When it &lt;a href=&quot;https://cursor.com/blog/bugbot-updates-june-2026&quot;&gt;started powering Bugbot&lt;/a&gt;, the reviews got “over 3x faster,” “22% cheaper,” and caught about 10% more bugs per review. The model’s tuned to do one focused job quickly and cheaply, which is exactly what you want for the daily grind of code review and small edits.&lt;/p&gt;
&lt;p&gt;Cursor is also pretty &lt;a href=&quot;https://cursor.com/blog/composer-2-technical-report&quot;&gt;open about how it’s built&lt;/a&gt;: Composer starts from an open checkpoint, Moonshot’s Kimi K2.5, with a load of continued pretraining and reinforcement learning layered on top. When that got out, some people treated it as a bit of a gotcha. “Oh, it’s just Kimi.”&lt;/p&gt;
&lt;p&gt;But that’s not the argument they think it is. Taking a strong open base and specialising it into a fast, cheap coding model is exactly the right move when proportion is the goal. You don’t need a frontier model’s full generality to go track down a CSS bug. Cursor took something already capable, narrowed it down, and made it cheap to run. If anything, the Kimi base is the most sensible thing about the whole approach.&lt;/p&gt;
&lt;h3 id=&quot;gpt-5s-router-the-cautionary-version&quot;&gt;GPT-5’s router: the cautionary version&lt;a class=&quot;heading-anchor&quot; href=&quot;#gpt-5s-router-the-cautionary-version&quot; aria-label=&quot;Permalink to GPT-5’s router: the cautionary version&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;OpenAI tried the same idea, only at scale, and got badly burned on the framing. The &lt;a href=&quot;https://techcrunch.com/2025/08/08/sam-altman-addresses-bumpy-gpt-5-rollout-bringing-4o-back-and-the-chart-crime/&quot;&gt;GPT-5 launch in August 2025&lt;/a&gt; replaced the model picker with an automatic router that decided, per prompt, which model should answer you. Then on launch day the autoswitcher broke. Altman himself admitted GPT-5 “seemed way dumber” and that OpenAI had “totally screwed up some things on the rollout.” People were convinced their prompts were being quietly shunted off to weaker, cheaper models, and whether or not that was the intent, that’s exactly how it landed. OpenAI walked it back fast: GPT-4o came back for paying users, the explicit Auto / Fast / Thinking controls showed up, and the company promised to be clearer about which model was actually answering.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/gpt5-chart-crime.D7EGr0l8_Z2pNqbh.webp&quot; alt=&quot;OpenAI’s GPT-5 launch chart for SWE-bench Verified, in which the 52.8% bar for GPT-5 without thinking is drawn taller than the 69.1% bar for OpenAI o3, and GPT-4o’s 30.8% bar is nearly as tall as o3’s.&quot;&gt;&lt;figcaption&gt;OpenAI’s GPT-5 launch chart for SWE-bench Verified, in which the 52.8% bar for GPT-5 without thinking is drawn taller than the 69.1% bar for OpenAI o3, and GPT-4o’s 30.8% bar is nearly as tall as o3’s.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;em&gt;It did not help that the launch itself shipped the now-infamous “chart crime”: GPT-5’s 52.8 drawn taller than o3’s 69.1. Source: OpenAI’s GPT-5 livestream, via &lt;a href=&quot;https://techcrunch.com/2025/08/08/sam-altman-addresses-bumpy-gpt-5-rollout-bringing-4o-back-and-the-chart-crime/&quot;&gt;TechCrunch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;And it’s the same underlying mechanism as Fable’s silent switch to Opus, automatic and per-prompt, only here it landed the complete opposite way. The routing itself was never really the problem. Not being able to see it happen was. On a product you’re paying for, an invisible downgrade just reads as a breach of trust rather than a feature.&lt;/p&gt;
&lt;h2 id=&quot;where-this-should-go-next&quot;&gt;Where this should go next&lt;a class=&quot;heading-anchor&quot; href=&quot;#where-this-should-go-next&quot; aria-label=&quot;Permalink to Where this should go next&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Which all brings me to what I actually want out of these tools.&lt;/p&gt;
&lt;p&gt;Claude Code’s heavier modes, the ultracode-style “fan out a swarm of agents and be exhaustive” workflows, are genuinely great when the task is big. But they tend to run the whole fleet at a single tier. And a lot of what those agents do is just legwork: grepping the codebase, summarising a file, checking a single claim. That’s not frontier-model work. You’d want the cheap, fast model doing the legwork and the expensive one held back for the synthesis and the genuinely hard reasoning.&lt;/p&gt;
&lt;p&gt;The primitive for this already exists. Workflows let you set the model per agent. What’s missing is a sensible default policy that does the routing for you, so the small, parallel, low-stakes subtasks go to the cheap model and the hard, central reasoning stays on the expensive one. That’s just proportion, productised. The governor, built in.&lt;/p&gt;
&lt;h2 id=&quot;so-not-a-failure&quot;&gt;So, not a failure&lt;a class=&quot;heading-anchor&quot; href=&quot;#so-not-a-failure&quot; aria-label=&quot;Permalink to So, not a failure&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;So no, I don’t read the Fable story as proof the model is dumb. I read it as a model that’s relentless by default, pointed at a problem far too small for it, with nothing yet in place to throttle it back down.&lt;/p&gt;
&lt;p&gt;Point that same relentlessness at a large, genuinely ambiguous problem with no obvious path, and it’s suddenly exactly the trait you want. The skill now isn’t really about getting the model to be smarter. It’s about matching the model to the size of the job, and, increasingly, about deciding how much you trust the platform to quietly make that match for you.&lt;/p&gt;
&lt;p&gt;Just make the routing something I can actually see. I’d much rather pick the right tool myself than find out, after the fact, that one quietly got picked for me.&lt;/p&gt;
&lt;aside class=&quot;callout callout-note&quot; role=&quot;note&quot;&gt;
&lt;div class=&quot;callout-title&quot;&gt;Update — 17 June&lt;/div&gt;
&lt;p&gt;I spent this whole post on who gets to decide which model you run: the model, the platform, a router. I missed a candidate. On 12 June, three days after Fable went public, &lt;a href=&quot;https://www.anthropic.com/news/fable-mythos-access&quot;&gt;the US government issued an export-control directive&lt;/a&gt; citing national security, and Anthropic disabled Fable 5 and Mythos 5 for everyone within roughly ninety minutes. The directive technically only bars foreign nationals, but since nobody can check your nationality mid-prompt, every customer lost access. The reported trigger was a claimed jailbreak that unlocked the vulnerability-finding skill Anthropic had once called too dangerous to ship, though Anthropic says the technique only surfaced minor, already-known bugs that other public models find anyway.&lt;/p&gt;
&lt;p&gt;It’s meant to be temporary, and Anthropic says it’s working to restore access. But for now the relentless model isn’t quietly downgrading itself to Opus. It’s just gone, and this time not one of us got a say.&lt;/p&gt;
&lt;/aside&gt;
</content:encoded></item><item><title>The Accidental Homelab</title><link>https://afonsojramos.me/blog/accidental-homelab/</link><guid isPermaLink="true">https://afonsojramos.me/blog/accidental-homelab/</guid><description>Nine years of self-hosting media, from Plex on a gaming PC to TrueNAS with 5×18TB raidz2 and ~30 apps. Built one budget at a time.</description><pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In 2016, as a university student, I installed Plex on my gaming desktop and started serving local media off it. The desktop was an i7-4770K with a GTX 770, the first PC I’d built myself, back in 2013 at 15. Nine years later, that same Plex install has grown into a TrueNAS server running ~30 self-hosted apps, ~40 TiB of usable storage on a raidz2 pool (with a story behind that number, &lt;a href=&quot;#what-writing-this-post-turned-up&quot;&gt;see below&lt;/a&gt;), and serving roughly 20 people across a few countries. None of it was planned. Every step came after the previous one started to break, and was paid for whenever the budget caught up.&lt;/p&gt;
&lt;nav class=&quot;table-of-contents&quot;&gt;&lt;ol&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#before-plex-20102016&quot; title=&quot;Before Plex (2010–2016)&quot;&gt;Before Plex (2010–2016)&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-gaming-rig-era-20162020&quot; title=&quot;The Gaming-Rig Era (2016–2020)&quot;&gt;The Gaming-Rig Era (2016–2020)&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-i7-4770k-as-always-on-server-20202023&quot; title=&quot;The i7-4770K as Always-On Server (2020–2023)&quot;&gt;The i7-4770K as Always-On Server (2020–2023)&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-2023-reset&quot; title=&quot;The 2023 Reset&quot;&gt;The 2023 Reset&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#why-truenas-and-not-unraid-or-proxmox&quot; title=&quot;Why TrueNAS (and Not Unraid or Proxmox)&quot;&gt;Why TrueNAS (and Not Unraid or Proxmox)&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-present-518tb-raidz2-30-apps-16-people&quot; title=&quot;The Present: 5×18TB raidz2, ~30 Apps, 16 People&quot;&gt;The Present: 5×18TB raidz2, ~30 Apps, 16 People&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-drive-failure-which-is-why-raidz2&quot; title=&quot;The Drive Failure (Which Is Why raidz2)&quot;&gt;The Drive Failure (Which Is Why raidz2)&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#what-writing-this-post-turned-up&quot; title=&quot;What Writing This Post Turned Up&quot;&gt;What Writing This Post Turned Up&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#no-regrets&quot; title=&quot;No Regrets&quot;&gt;No Regrets&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/nav&gt;&lt;h2 id=&quot;before-plex-20102016&quot;&gt;Before Plex (2010–2016)&lt;a class=&quot;heading-anchor&quot; href=&quot;#before-plex-20102016&quot; aria-label=&quot;Permalink to Before Plex (2010–2016)&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I’d been torrenting since 2010. I was twelve, &lt;a href=&quot;https://en.wikipedia.org/wiki/UTorrent&quot;&gt;uTorrent&lt;/a&gt; was the client, and the workflow was strictly manual: pick the show or movie I wanted, search for it, find a healthy seed, download, watch. The uTorrent download folder &lt;em&gt;was&lt;/em&gt; my entire media library. Half of it was things I’d already watched, half was things I was queuing up for the weekend, and the only “library management” was my own memory plus a &lt;a href=&quot;https://en.wikipedia.org/wiki/TVShowTime&quot;&gt;TV Show Time&lt;/a&gt; account that kept track of which episode of which show I was up to.&lt;/p&gt;
&lt;p&gt;This was the &lt;a href=&quot;https://en.wikipedia.org/wiki/KickassTorrents&quot;&gt;KickassTorrents&lt;/a&gt; era, and KAT was awesome - moderators, comment threads on every release, a community that would flag bad rips and call out fakes. The site got taken offline in a &lt;a href=&quot;https://www.justice.gov/archives/opa/pr/us-authorities-charge-owner-most-visited-illegal-file-sharing-website-copyright-infringement&quot;&gt;US-led operation&lt;/a&gt; in 2016, and the gap eventually got filled, for me at least, by private trackers, where ratios are enforced and quality gets policed much more directly. Tens of terabytes of seeding later, I’m still on the same ones.&lt;/p&gt;
&lt;p&gt;Plex, when I found it in 2016, was the abstraction I’d been missing. It turned that flat folder of files into something I could actually browse - metadata, posters, subtitles, watch progress - and, more importantly, share with people who weren’t going to learn what &lt;code&gt;.mkv&lt;/code&gt; meant.&lt;/p&gt;
&lt;h2 id=&quot;the-gaming-rig-era-20162020&quot;&gt;The Gaming-Rig Era (2016–2020)&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-gaming-rig-era-20162020&quot; aria-label=&quot;Permalink to The Gaming-Rig Era (2016–2020)&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The first version was the laziest possible setup: Plex on my main Windows desktop, drives merged with Windows’ built-in Storage Spaces so 2×1TB + 1×4TB looked like a single mount point, port-forward so a couple of family members could stream from outside. The “server” was whatever happened to be on the gaming PC at the time. I held out on Plex Pass until 2018, when I caught one of their 50%-off lifetime sales - still easily the best money I’ve ever spent on this whole project.&lt;/p&gt;
&lt;p&gt;It worked exactly as well as you’d expect. Plex transcoding is CPU-bound, gaming is GPU-bound, but they both fight for memory bandwidth and disk IO. Three people watching from outside the house on shaky connections, and my gaming session noticed. I could pause my game, kick people, or accept that nobody’s experience was going to be great. But to be honest, this did not happen that often back then.&lt;/p&gt;
&lt;h2 id=&quot;the-i7-4770k-as-always-on-server-20202023&quot;&gt;The i7-4770K as Always-On Server (2020–2023)&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-i7-4770k-as-always-on-server-20202023&quot; aria-label=&quot;Permalink to The i7-4770K as Always-On Server (2020–2023)&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In 2020, with my first salary out of university, I bought a new gaming desktop - Ryzen 9 5900X, RTX 2060 Super - and “retired” the i7-4770K. Retired meaning “moved to a corner with the drives still attached and never turned off again.” First time the home server became a separate machine from my gaming PC.&lt;/p&gt;
&lt;p&gt;The stack matured a bit. Sonarr and Radarr came online (or maybe before, not entirely sure when I brought these on); access to a couple of decent private trackers and a few reliable public ones meant new media just showed up without me babysitting it. Parsec gave me remote access. Overseerr arrived later and took requests off my hands - friends and family could ask for content directly (I did share *arr access for a bit before this).&lt;/p&gt;
&lt;p&gt;This is also when the user count grew past family, and transcoding got real. Three concurrent remote streams on the 4770K’s iGPU was the limit. I never wanted to disable transcoding entirely (some people had genuinely bad upstream connections), but the 4770K just didn’t have the headroom.&lt;/p&gt;
&lt;p&gt;The whole thing was still on Windows. NTFS had a lot of data on it; a clean Linux migration was always “next year.” I knew Storage Spaces would haunt me eventually, but it kept working. Should I have just shipped the whole library to S3 and dealt with the migration cold? Probably. But moving 8 TB of Linux ISOs to a cloud bucket on a non-fiber upload is a project of its own (to be fair, we did eventually get fiber a year or two later).&lt;/p&gt;
&lt;p&gt;In 2021 I bought my first 18 TB drive. Single drive, no redundancy, “I’ll figure out RAID later.” That made the eventual migration even more daunting with even more Linux ISOs to migrate!&lt;/p&gt;
&lt;h2 id=&quot;the-2023-reset&quot;&gt;The 2023 Reset&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-2023-reset&quot; aria-label=&quot;Permalink to The 2023 Reset&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;By 2023 the smell was real. I wanted out of Windows, I wanted real ZFS, and I wanted a server that wasn’t living in fear of a Windows Update at 03. I also didn’t want to take the existing setup down while building the new one - there were ~16 people relying on this thing by then, and “your media is broken for two weeks” wasn’t something I wanted to do to anyone.&lt;/p&gt;
&lt;p&gt;So I bought a used PC from a local secondhand site: Ryzen 7 5800X, RTX 3080, 64 GB of RAM. The point of the deal wasn’t the 5800X - it was the GPU and the memory. I swapped both into the 5900X gaming rig (which got a meaningful upgrade for almost nothing), stripped the used box down to CPU + motherboard + chassis, and stood it up next to the running 4770K as the new server. Built the new platform fully, copied data across, swapped people over. User-facing downtime was a few hours.&lt;/p&gt;
&lt;h2 id=&quot;why-truenas-and-not-unraid-or-proxmox&quot;&gt;Why TrueNAS (and Not Unraid or Proxmox)&lt;a class=&quot;heading-anchor&quot; href=&quot;#why-truenas-and-not-unraid-or-proxmox&quot; aria-label=&quot;Permalink to Why TrueNAS (and Not Unraid or Proxmox)&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Most homelab content for media servers points at Unraid: mixed disk sizes, single parity, paid license, very forgiving of “I bought one drive at a time over four years.” For someone whose drive collection grew organically - like mine had - Unraid is the obvious answer.&lt;/p&gt;
&lt;p&gt;I went with TrueNAS Community Edition (formerly TrueNAS Scale) for three reasons.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ZFS is first-class.&lt;/strong&gt; Unraid only added real ZFS support recently, and even then its core array layer is XFS-per-disk with parity. ZFS gives me snapshots, scrubs, send/recv, end-to-end checksums, and a pool layer I trust. After a year of running this I’ve caught zero silent bit-rot, but it’s the kind of thing you only know is working because nothing’s gone wrong.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;All same-size disks anyway.&lt;/strong&gt; My organic-growth phase ended in 2021. Going forward I knew I’d buy 18 TB drives until I died - they sit at the sweet spot for €/TB. Once you commit to one drive size, Unraid’s flexibility advantage evaporates and ZFS’s rigidity stops mattering.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The apps platform.&lt;/strong&gt; TrueNAS’s built-in app catalog runs containers as first-class objects on the host, with iXVolumes (ZFS-backed) for app state. I don’t need a separate Proxmox + LXC + manual ZFS layer. One UI, one auth, one upgrade flow.&lt;/p&gt;
&lt;p&gt;If you have ten differently-sized drives accumulated over a decade, get Unraid. If you’re starting fresh and willing to commit to one size, get TrueNAS. I did wipe my old non-18TB drives and sold them, by the way.&lt;/p&gt;
&lt;h2 id=&quot;the-present-518tb-raidz2-30-apps-16-people&quot;&gt;The Present: 5×18TB raidz2, ~30 Apps, 16 People&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-present-518tb-raidz2-30-apps-16-people&quot; aria-label=&quot;Permalink to The Present: 5×18TB raidz2, ~30 Apps, 16 People&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Today the pool is five 18 TB Seagate IronWolf Pros in a single raidz2 vdev: two-disk redundancy, ~40 TiB usable today (currently ~79% full), with about 7 TiB stuck behind a legacy parity layout that I only noticed while writing this post - more on that further down. Single-vdev is fine here because the IOPS-sensitive apps run on a separate pool - ZFS scales IOPS per vdev, so a 5-disk raidz2 has the random-read throughput of roughly one disk, which doesn’t matter for sequential media streaming. Most of the drives came from &lt;a href=&quot;https://serverpartdeals.com&quot;&gt;ServerPartDeals&lt;/a&gt;, a recertified-disk shop that ships enterprise drives at a meaningful discount over new. Worked out at roughly half of new-IronWolf-Pro pricing, the trade-off being that the 5-year warranty runs from manufacture date rather than purchase date.&lt;/p&gt;
&lt;p&gt;The host is on a UPS that easily handles half an hour of operation. Nothing’s port-forwarded anymore: family-facing apps come in through a Cloudflare Tunnel at &lt;code&gt;*.afonsojramos.me&lt;/code&gt; (no exposed home IP, TLS handled at the edge), and everything admin - the *arrs, qBittorrent, dashboards, the lot - stays behind Tailscale. Two access tiers, one running it, the other consuming it. The stack, in rough shape:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;storage: 5×18TB raidz2 plus a separate &lt;code&gt;apps&lt;/code&gt; pool for fast app state&lt;/li&gt;
&lt;li&gt;media: Plex (1,400+ movies, 21 TB of TV, 874 GB of music), Jellyfin alongside it for redundancy (and external downloads, since the Plex Pass only allows Home User downloads), Tautulli for stats, Maintainerr for retention rules&lt;/li&gt;
&lt;li&gt;photos: Immich&lt;/li&gt;
&lt;li&gt;cloud: Seafile, plus Filebrowser and SMB shares for the unencrypted stuff&lt;/li&gt;
&lt;li&gt;auth: Authentik fronts SSO across the public apps and gates the admin tools behind a &lt;code&gt;homelab-admins&lt;/code&gt; group; Wizarr at &lt;code&gt;invite.afonsojramos.me&lt;/code&gt; is the family onboarding door, bootstrapping both Plex and Jellyfin from a single invite&lt;/li&gt;
&lt;li&gt;plumbing: Sonarr, Radarr, Bazarr, Overseerr, qBittorrent, Dockge, Glance, plus a handful of custom Dockge stacks for things the catalog doesn’t ship&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/grafana.BB9aT18J_Z1f4S7p.webp&quot; alt=&quot;Grafana dashboard showing 83 running containers across the homelab at 0.481 CPU cores and 16.3 GiB of memory, with zero restarts and per-service CPU, memory, network, and restart breakdowns.&quot;&gt;&lt;figcaption&gt;Grafana dashboard showing 83 running containers across the homelab at 0.481 CPU cores and 16.3 GiB of memory, with zero restarts and per-service CPU, memory, network, and restart breakdowns.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;the-drive-failure-which-is-why-raidz2&quot;&gt;The Drive Failure (Which Is Why raidz2)&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-drive-failure-which-is-why-raidz2&quot; aria-label=&quot;Permalink to The Drive Failure (Which Is Why raidz2)&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A few weeks ago, &lt;a href=&quot;https://github.com/AnalogJ/scrutiny&quot;&gt;Scrutiny&lt;/a&gt; - the SMART dashboard I have watching the pool - lit up with UNC errors against &lt;code&gt;/dev/sdd&lt;/code&gt;, one of the recertified IronWolf Pros (8,682 power-on hours, roughly a year of always-on, 18 TB), and a failed extended self-test soon after. It’s still online, but the pool reports &lt;code&gt;READ=1&lt;/code&gt; against it. The drive is firmly dying.&lt;/p&gt;
&lt;p&gt;This is why raidz2 exists. With raidz1 (single-disk redundancy) you can lose another disk during a 4–6 day resilver window, which is a real risk: large drives mean long resilvers, the resilver itself stresses the remaining disks, and those remaining disks are also a year old and also from the same recertified batch. With raidz2 I can lose this one and another during the resilver and still be fine. The math on a five-disk pool of 18 TB drives wants two parity disks.&lt;/p&gt;
&lt;p&gt;The drive came from ServerPartDeals; the RMA paperwork is in flight. The vdev still has both parity disks intact while I’m waiting on it, so the cost so far is “wait and watch.” And I’ve also ordered a new drive, which I’ll add to the pool once it arrives.&lt;/p&gt;
&lt;h2 id=&quot;what-writing-this-post-turned-up&quot;&gt;What Writing This Post Turned Up&lt;a class=&quot;heading-anchor&quot; href=&quot;#what-writing-this-post-turned-up&quot; aria-label=&quot;Permalink to What Writing This Post Turned Up&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When I expanded this pool from 4-wide to 5-wide raidz2 with a &lt;code&gt;zpool attach&lt;/code&gt; almost a year ago, I did the napkin math for the new geometry - 5×18 TB raidz2, ~49 TiB raw, ~44 TiB usable after ZFS overhead - and then never went back to check whether the running pool actually matched. The number sat in my head as fact for almost a year. Writing this section was the first time I’d looked at it again, and it turns out that that is not what I’ve been looking at at all.&lt;/p&gt;
&lt;p&gt;I am currently getting ~40 TiB total, ~4 TiB short of the number I’d been carrying around since the expansion. Turns out that OpenZFS raidz expansion has a footnote I’d glossed over: it only changes the layout for new writes. Records already on disk keep their pre-expansion parity ratio forever. 4-wide raidz2 is 50% parity (2 of 4 disks); 5-wide is 40% (2 of 5). Everything written before the expansion still pays the 4-wide rate, and nothing in &lt;code&gt;zpool&lt;/code&gt; or &lt;code&gt;zfs&lt;/code&gt; will restripe existing records - not scrub, not resilver, not property changes. I had, honestly, assumed that that would be done in the expansion process, but it turns out that the only fix is to rewrite the data so ZFS reallocates it under the current geometry.&lt;/p&gt;
&lt;p&gt;The tool for that is &lt;a href=&quot;https://github.com/markusressel/zfs-inplace-rebalancing&quot;&gt;markusressel/zfs-inplace-rebalancing.sh&lt;/a&gt;: walks the dataset, copies each file to a sibling tempfile (no &lt;code&gt;--reflink&lt;/code&gt;, so it’s a real fresh write), verifies size and checksum, atomic &lt;code&gt;mv&lt;/code&gt; back over the original. Resumable across runs. Napkin math says I should reclaim about 7 TiB on ~36 TB of pre-expansion media - &lt;code&gt;36 × (1 − 40/50) = 7.2&lt;/code&gt; - which would push the pool back past the ~44 TiB I’d always had in my head.&lt;/p&gt;
&lt;p&gt;The catch: it’s a multi-day full-pool I/O storm, and &lt;code&gt;/dev/sdd&lt;/code&gt; is exactly the wrong disk to be running it against. After replacing it I’ll need to resilver, scrub, and only then rebalance.&lt;/p&gt;
&lt;p&gt;The takeaway, if you’re starting fresh: &lt;code&gt;zpool attach&lt;/code&gt; raidz expansion is a one-shot operation you should plan around, not a casual capacity-add. Either commit to a rebalance pass after every expansion, or grow the pool by replacing all five disks one-by-one with larger ones (&lt;code&gt;autoexpand=on&lt;/code&gt;), which keeps the geometry constant and avoids the legacy-tier problem entirely. Given &lt;code&gt;/dev/sdd&lt;/code&gt; is going RMA anyway, the replacement could itself be the first 20 TB drive in a gradual disk-size upgrade - no parity-ratio drift, no rebalance debt, just bigger drives over time.&lt;/p&gt;
&lt;aside class=&quot;callout callout-note&quot; role=&quot;note&quot;&gt;
&lt;div class=&quot;callout-title&quot;&gt;Edit — 29 May 2026&lt;/div&gt;
&lt;p&gt;I did all of the above, and it worked. The replacement drive turned out to be a Toshiba MG09 18 TB rather than a 20 TB - a same-size swap, but it breaks up the all-Seagate, all-same-batch monoculture, which is good in my books. It was also a drive that had a good reputation in Backblaze’s reports.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;zpool replace&lt;/code&gt; resilvered in 39 hours at full redundancy (online replace - the failing disk stayed in the array the whole time, so parity never dropped to raidz1), a verification scrub came back clean, and then I let the rebalance run. It took &lt;strong&gt;9.5 days&lt;/strong&gt; for ~32 TB - I’d budgeted 3-5, and the estimate was about half what it should’ve been, because the cost is per-file &lt;code&gt;cp&lt;/code&gt;/verify overhead, not throughput: a 30 GB remux and a 2 KB &lt;code&gt;.nfo&lt;/code&gt; each pay the same fixed tax, and the TV library alone is 31,000 files. In total, it reclaimed &lt;strong&gt;5.2 TiB&lt;/strong&gt; (the pool went from 82% to 75% full), a bit under the 7.2 I’d predicted because a chunk of the library was written &lt;em&gt;after&lt;/em&gt; the expansion and was already on the efficient layout. Two gotchas worth flagging if you try this on TrueNAS: the rebalance script’s &lt;code&gt;cp -ax&lt;/code&gt; dies instantly on datasets with &lt;code&gt;aclmode=restricted&lt;/code&gt; (flip it to &lt;code&gt;passthrough&lt;/code&gt; for the run), and any snapshot predating the rebalance pins the old-layout blocks so the pool &lt;em&gt;grows&lt;/em&gt; as you go - I had to drop five stale snapshots mid-run to avoid filling the pool before it could shrink. The honest verdict: the rebalance is the rare homelab chore that delivers exactly what the napkin math promises.&lt;/p&gt;
&lt;/aside&gt;
&lt;h2 id=&quot;no-regrets&quot;&gt;No Regrets&lt;a class=&quot;heading-anchor&quot; href=&quot;#no-regrets&quot; aria-label=&quot;Permalink to No Regrets&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Looking back, I don’t really have any. Self-hosting in 2016 was a much lonelier hobby than it is in 2026. No &lt;a href=&quot;https://reddit.com/r/selfhosted&quot;&gt;/r/selfhosted&lt;/a&gt; at its current size, no Tailscale, no TrueNAS Community Edition, no recertified-drive economy you could trust at scale, and homelab content on YouTube was a tiny fraction of what it is today. Most of what looks obvious in 2026 had to be figured out from scratch back then, and a lot of the choices that look “wrong” in retrospect were the only realistic option at the time.&lt;/p&gt;
&lt;p&gt;The hurdles were also good for me. Storage Spaces taught me what I wanted from a real filesystem. Running Plex on my gaming PC taught me why isolation matters. The Windows years taught me what I was willing to pay for in operational simplicity. None of that would have landed the same way if I’d been handed today’s tooling on day one.&lt;/p&gt;
&lt;p&gt;A few opinions that have held up, regardless of when you start:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Storage Spaces is fine until it isn’t. Use it, then leave it before your library makes the migration scary.&lt;/li&gt;
&lt;li&gt;Don’t run Plex on your gaming PC. The moment your library has more than a couple of viewers, it deserves its own machine.&lt;/li&gt;
&lt;li&gt;Recertified enterprise drives are great once you trust the supplier. ServerPartDeals’ IronWolf Pros land at roughly half the price of new for the warranty trade-off.&lt;/li&gt;
&lt;li&gt;Pay for Plex Pass when it goes on sale. The 50%-off lifetime promos come around. Best deal in self-hosting. (Edit (29 May 2026): turns out, this was an even better deal than I thought, since from July 2026, &lt;a href=&quot;https://9to5mac.com/2026/05/19/plex-increasing-lifetime-plex-pass-cost-to-whopping-750/&quot;&gt;Plex Pass will be available for 750$&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The thing the homelab guides on YouTube don’t tell you is that it never actually starts as a homelab. It starts as one Plex install on whatever’s nearest, and a few years later you catch yourself reading about ECC memory on a Sunday afternoon.&lt;/p&gt;
</content:encoded></item><item><title>The Abstraction Imperative: Why Framework-Defined Infrastructure is the Next Step in Software Evolution</title><link>https://afonsojramos.me/blog/framework-defined-infrastructure/</link><guid isPermaLink="true">https://afonsojramos.me/blog/framework-defined-infrastructure/</guid><description>Every major leap in software development has followed the same pattern: we abstract away complexity that no longer needs manual attention. Framework-defined infrastructure isn&apos;t just another deployment strategy, it&apos;s the continuation of a decades-long trajectory toward letting developers focus on what actually matters.</description><pubDate>Sat, 22 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every major leap in software development has followed the same pattern: we abstract away complexity that no longer needs manual attention. &lt;a href=&quot;https://vercel.com/blog/framework-defined-infrastructure&quot;&gt;Framework-defined infrastructure&lt;/a&gt; is not merely another deployment strategy; it is the logical continuation of a decades-long trajectory allowing developers to focus on the problem, not the plumbing. At no point in this blog post will I argue that we don’t need plumbers. Instead, I will argue that we should worry less about plumbing by trusting well-tested abstractions that others have carefully refined over time.&lt;/p&gt;
&lt;nav class=&quot;table-of-contents&quot;&gt;&lt;ol&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-history-of-software-is-the-history-of-abstraction&quot; title=&quot;The History of Software is the History of Abstraction&quot;&gt;The History of Software is the History of Abstraction&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#where-we-are-now-the-infrastructure-abstraction-gap&quot; title=&quot;Where We Are Now: The Infrastructure Abstraction Gap&quot;&gt;Where We Are Now: The Infrastructure Abstraction Gap&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#framework-defined-infrastructure-closes-the-gap&quot; title=&quot;Framework-Defined Infrastructure Closes the Gap&quot;&gt;Framework-Defined Infrastructure Closes the Gap&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#why-this-matters-beyond-any-single-platform&quot; title=&quot;Why This Matters Beyond Any Single Platform&quot;&gt;Why This Matters Beyond Any Single Platform&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-developer-experience-transformation&quot; title=&quot;The Developer Experience Transformation&quot;&gt;The Developer Experience Transformation&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#abstraction-enables-specialization&quot; title=&quot;Abstraction Enables Specialization&quot;&gt;Abstraction Enables Specialization&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-economics-of-abstraction&quot; title=&quot;The Economics of Abstraction&quot;&gt;The Economics of Abstraction&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#what-this-means-for-the-industry&quot; title=&quot;What This Means for the Industry&quot;&gt;What This Means for the Industry&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-evolution-continues&quot; title=&quot;The Evolution Continues&quot;&gt;The Evolution Continues&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#conclusion-abstraction-is-inevitable&quot; title=&quot;Conclusion: Abstraction is Inevitable&quot;&gt;Conclusion: Abstraction is Inevitable&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/nav&gt;&lt;h2 id=&quot;the-history-of-software-is-the-history-of-abstraction&quot;&gt;The History of Software is the History of Abstraction&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-history-of-software-is-the-history-of-abstraction&quot; aria-label=&quot;Permalink to The History of Software is the History of Abstraction&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In the 1950s, programmers wrote in machine code, toggling switches to represent binary instructions. Assembly language abstracted machine code. High-level languages like C abstracted assembly. Garbage collection abstracted memory management. Virtual machines abstracted operating systems. Cloud computing abstracted physical hardware.&lt;/p&gt;
&lt;p&gt;Each abstraction faced resistance. &lt;em&gt;“Real programmers”&lt;/em&gt; supposedly needed direct memory access. Garbage collection was &lt;em&gt;“too slow”&lt;/em&gt; for serious applications. Virtual machines added &lt;em&gt;“unnecessary overhead.”&lt;/em&gt; Cloud computing meant &lt;em&gt;“losing control.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In each case, the abstraction became dominant, though not universal. Not because it was perfect, but because it let most developers solve most problems at a higher level. Assembly still exists for embedded systems, manual memory management remains critical for certain performance-sensitive applications, and some organizations still run their own data centers. The question was never whether to abstract completely, but when the abstraction became &lt;strong&gt;good enough&lt;/strong&gt; to trust &lt;strong&gt;for the majority of use cases&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&quot;where-we-are-now-the-infrastructure-abstraction-gap&quot;&gt;Where We Are Now: The Infrastructure Abstraction Gap&lt;a class=&quot;heading-anchor&quot; href=&quot;#where-we-are-now-the-infrastructure-abstraction-gap&quot; aria-label=&quot;Permalink to Where We Are Now: The Infrastructure Abstraction Gap&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Modern application development has reached an inflection point. We’ve abstracted nearly everything about software except how we deploy it.&lt;/p&gt;
&lt;p&gt;The move to web applications itself represents a massive abstraction, instead of writing platform-specific native code for Windows, macOS, Linux, iOS, and Android, developers write once for the browser and let the browser handle platform differences. APIs and microservices abstracted monolithic architectures. Containerization abstracted runtime environments. The pattern is consistent: each generation removes another layer of manual platform-specific work.&lt;/p&gt;
&lt;p&gt;Yet deployment remains stubbornly manual. We’ve moved beyond FTPing files to servers, yes, but consider a typical web application today. Developers write code using framework conventions, routes, components, server functions, middleware. But when deployment time comes, they must translate these high-level concepts into low-level infrastructure primitives: Lambda functions, API Gateways, load balancers, CDN configurations, caching layers.&lt;/p&gt;
&lt;p&gt;This translation layer is where things break down. As the evolution of Platform as a Service (PaaS) has shown, &lt;a href=&quot;https://dzone.com/articles/platform-as-a-service-paas-origins-and-architectur&quot;&gt;the progression from bare metal to virtualized infrastructure represents “the evolutionary nature” of modern software&lt;/a&gt;. Yet even with traditional PaaS offerings, developers still face somewhat of a gap between application code and infra configuration.&lt;/p&gt;
&lt;p&gt;With traditional Infrastructure as Code (IaC) approaches, you write a server-side rendering function in your framework, then spend hours configuring the serverless function, its memory limits, timeout settings, IAM roles, and API Gateway integration. The gap between framework concept and infrastructure primitive remains stubbornly manual. Terraform fatigue is real.&lt;/p&gt;
&lt;h2 id=&quot;framework-defined-infrastructure-closes-the-gap&quot;&gt;Framework-Defined Infrastructure Closes the Gap&lt;a class=&quot;heading-anchor&quot; href=&quot;#framework-defined-infrastructure-closes-the-gap&quot; aria-label=&quot;Permalink to Framework-Defined Infrastructure Closes the Gap&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Framework-defined infrastructure eliminates this translation layer by making the framework itself the interface for infrastructure decisions. As &lt;a href=&quot;https://vercel.com/blog/framework-defined-infrastructure&quot;&gt;Vercel describes it&lt;/a&gt;, &lt;em&gt;“the deployment environment automatically provisions infrastructure derived from the framework and the applications written in it.”&lt;/em&gt; The platform reads your framework code and provisions infrastructure automatically based on what it understands your code needs.&lt;/p&gt;
&lt;p&gt;This isn’t an entirely new concept. In fact, the roots trace back to 2006 when Zimki introduced what Simon Wardley called &lt;a href=&quot;https://en.wikipedia.org/wiki/Platform_as_a_service&quot;&gt;“framework-as-a-service”&lt;/a&gt;, a fascinating early attempt that could have made &lt;a href=&quot;https://www.porter.run/blog/history-of-paas-how-canon-almost-became-a-major-cloud-provider&quot;&gt;Canon one of the first major cloud providers&lt;/a&gt; before the industry settled on the term Platform as a Service. Though Zimki ultimately closed, &lt;a href=&quot;https://www.heroku.com/about/&quot;&gt;Heroku launched in 2007&lt;/a&gt; and successfully commercialized this vision with their mantra that developers should “focus on what they do best: building great apps” while the platform handles infrastructure. Heroku’s architecture was designed to &lt;a href=&quot;https://www.heroku.com/about/&quot;&gt;remove obstacles so developers can focus on building&lt;/a&gt; rather than managing servers.&lt;/p&gt;
&lt;p&gt;Similarly, Netlify popularized the JAMstack architecture, a term &lt;a href=&quot;https://www.netlify.com/jamstack/&quot;&gt;coined by CEO Mathias Biilmann&lt;/a&gt;, which emphasizes prerendering content and deploying directly to the edge. As Netlify explains, their approach is fundamentally about &lt;a href=&quot;https://www.netlify.com/jamstack/&quot;&gt;“abstraction and simplicity”&lt;/a&gt;, where build automation understands framework patterns and automatically optimizes deployments.&lt;/p&gt;
&lt;p&gt;The framework conventions themselves become infrastructure declarations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When you export &lt;code&gt;getServerSideProps&lt;/code&gt; in Next.js, you’re declaring that route needs server-side compute&lt;/li&gt;
&lt;li&gt;When you set &lt;code&gt;output: &apos;server&apos;&lt;/code&gt; in Astro, you’re declaring the entire site needs SSR capabilities&lt;/li&gt;
&lt;li&gt;When you create a &lt;code&gt;+page.server.ts&lt;/code&gt; in SvelteKit, you’re declaring that route requires server-side data loading&lt;/li&gt;
&lt;li&gt;When you use &lt;code&gt;export const prerender = false&lt;/code&gt; in SolidStart, you’re opting that route into dynamic rendering&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The framework has already made the infrastructure decision, it just needs a platform that understands it.&lt;/strong&gt; This isn’t magic. It’s pattern recognition applied to infrastructure. Frameworks impose structure and conventions specifically to make code predictable and understandable. Framework-defined infrastructure leverages that predictability to automate infrastructure provisioning the same way frameworks automate application structure.&lt;/p&gt;
&lt;h2 id=&quot;why-this-matters-beyond-any-single-platform&quot;&gt;Why This Matters Beyond Any Single Platform&lt;a class=&quot;heading-anchor&quot; href=&quot;#why-this-matters-beyond-any-single-platform&quot; aria-label=&quot;Permalink to Why This Matters Beyond Any Single Platform&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The power of framework-defined infrastructure isn’t tied to any particular deployment platform.&lt;/strong&gt; Multiple platforms, Vercel, Netlify, Cloudflare Pages, AWS Amplify, Azure Static Web Apps, have independently converged on the same insight: let the framework define infrastructure needs. Though implementations vary in how much configurability they expose, the core principle remains consistent.&lt;/p&gt;
&lt;p&gt;This convergence validates the abstraction. Just as multiple operating systems implement virtual memory and multiple languages implement garbage collection, multiple platforms implementing framework-defined infrastructure proves the concept has staying power.&lt;/p&gt;
&lt;p&gt;As Vercel notes in their piece on &lt;a href=&quot;https://vercel.com/blog/vercel-the-anti-vendor-lock-in-cloud&quot;&gt;avoiding vendor lock-in&lt;/a&gt;, “Framework conventions like the Next.js App Router, Remix loaders, SvelteKit endpoints, and Nitro storage adapters work differently. You build against the framework, not the platform. Multiple platforms can run the same framework code, keeping your application portable across any infrastructure that supports it.”&lt;/p&gt;
&lt;p&gt;The competitive advantage shifts from proprietary primitives to better implementations of the same abstraction. Platforms compete on performance, developer experience, observability, and pricing, not on lock-in. In fact, approximately 70% of Next.js applications run outside of Vercel, with companies like Walmart, Nike, and Claude.ai self-hosting at massive scale.&lt;/p&gt;
&lt;p&gt;There are legitimate concerns about vendor lock-in when platforms handle multiple aspects of your stack. As one developer &lt;a href=&quot;https://gomakethings.com/the-challenge-with-netlify-vercel-cloudflare-and-so-on/&quot;&gt;notes&lt;/a&gt;, “when one provider handles your hosting, manages automated deployment, runs a few dozen micro-services and serverless APIs, provides your database through their proprietary API, and more, migrating away to somewhere else becomes very expensive.” But framework-defined infrastructure actually addresses this concern by keeping the abstraction layer in the open-source framework, not in proprietary platform APIs.&lt;/p&gt;
&lt;h2 id=&quot;the-developer-experience-transformation&quot;&gt;The Developer Experience Transformation&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-developer-experience-transformation&quot; aria-label=&quot;Permalink to The Developer Experience Transformation&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The real benefit surfaces in daily development work. With framework-defined infrastructure, your local development environment matches production behavior because both run the same framework code. There’s no simulation layer, no mocked services, no “works on my machine” debugging.&lt;/p&gt;
&lt;p&gt;As Vercel explains, platforms built on vendor primitives need complex simulators for local development, Cloudflare provides Wrangler to simulate Workers locally, AWS developers use LocalStack or SAM CLI to mock Lambda and other services. These tools approximate production behavior but never match it exactly.&lt;/p&gt;
&lt;p&gt;This echoes Heroku’s original insight: by &lt;a href=&quot;https://www.porter.run/blog/what-is-heroku&quot;&gt;abstracting away the underlying infrastructure&lt;/a&gt;, developers “don’t have to worry about anything except for application logic.” With framework-defined infrastructure, you test your application by running &lt;code&gt;next dev&lt;/code&gt; or &lt;code&gt;remix dev&lt;/code&gt;, the standard framework tooling. What you see locally is what runs in production because the framework, not the platform, defines the behavior.&lt;/p&gt;
&lt;p&gt;This eliminates an entire class of problems. You don’t need to maintain separate local development simulators like LocalStack or Wrangler. You don’t need to keep IaC configurations in sync with application changes. You don’t need platform-specific tooling that only half-works on your operating system.&lt;/p&gt;
&lt;p&gt;Tools like &lt;a href=&quot;https://sst.dev/&quot;&gt;SST&lt;/a&gt; represent an interesting middle ground—they use framework conventions to &lt;em&gt;generate&lt;/em&gt; IaC (CloudFormation/Terraform) rather than bypassing it entirely. This hybrid approach still requires maintaining infrastructure code, but reduces the manual translation burden by inferring infrastructure needs from your application code.&lt;/p&gt;
&lt;h2 id=&quot;abstraction-enables-specialization&quot;&gt;Abstraction Enables Specialization&lt;a class=&quot;heading-anchor&quot; href=&quot;#abstraction-enables-specialization&quot; aria-label=&quot;Permalink to Abstraction Enables Specialization&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Critics worry that abstraction means losing control. But abstraction doesn’t eliminate control, it enables specialization.&lt;/p&gt;
&lt;p&gt;When you write a Next.js application, you’re not locked out of infrastructure. You can still deploy to EC2 and manage every detail if needed. Companies like Walmart and Nike do exactly this at massive scale. The difference is that you can also choose not to, letting platforms that specialize in infrastructure handle those concerns while you specialize in your product.&lt;/p&gt;
&lt;p&gt;This is how every successful abstraction works. You can still write assembly if you need to, but most developers rightfully choose to write in higher-level languages and let the compiler handle optimization. You can still manage memory manually, but most developers let garbage collection handle it. You can still provision servers, but most developers let cloud platforms handle it.&lt;/p&gt;
&lt;p&gt;As the evolution of cloud computing shows, &lt;a href=&quot;https://cloud.google.com/learn/paas-vs-iaas-vs-saas&quot;&gt;PaaS sits between IaaS and SaaS&lt;/a&gt; specifically to “abstract infrastructure complexities, allowing developers to focus on building and innovating.” Framework-defined infrastructure extends this progression. You can still configure infrastructure manually if you need to, but most developers can let the framework and platform handle it automatically.&lt;/p&gt;
&lt;h2 id=&quot;the-economics-of-abstraction&quot;&gt;The Economics of Abstraction&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-economics-of-abstraction&quot; aria-label=&quot;Permalink to The Economics of Abstraction&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Abstractions succeed when the economic value of simplification exceeds the cost of flexibility lost. Framework-defined infrastructure passes this test decisively.&lt;/p&gt;
&lt;p&gt;Consider the engineering cost of a typical deployment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Writing and maintaining IaC configurations&lt;/li&gt;
&lt;li&gt;Keeping infrastructure in sync with application changes&lt;/li&gt;
&lt;li&gt;Managing environment differences between development and production&lt;/li&gt;
&lt;li&gt;Training new team members on platform-specific tooling&lt;/li&gt;
&lt;li&gt;Debugging issues caused by misconfiguration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now consider the alternative: writing framework code that automatically deploys correctly. As Vercel found when studying &lt;a href=&quot;https://vercel.com/blog/accelerating-developer-velocity-and-creating-high-impact-web-teams&quot;&gt;high-impact web teams&lt;/a&gt;, “when developers control their entire workflow, teams previously balancing both hardware and software can focus solely on the strategic aspects of product delivery. Conversations shift from ‘how’ and ‘if’ to ‘what’ and ‘when,’ as infrastructure is no longer an obstacle to launching products.”&lt;/p&gt;
&lt;p&gt;The reduction in cognitive load, time spent, and error rate represents genuine economic value. A Forrester Total Economic Impact report found that teams using framework-defined infrastructure saw higher customer conversion rates generating $2.6 million in incremental profits and higher website traffic generating $7.7 million in incremental profits.&lt;/p&gt;
&lt;p&gt;Netlify’s experience with the JAMstack architecture demonstrates similar benefits. By &lt;a href=&quot;https://www.netlify.com/blog/2020/04/01/automate-your-web-workflows-with-the-jamstack/&quot;&gt;automating the build and deploy process&lt;/a&gt;, platforms ensure “code can be shipped with little to no opportunity for error,” while the architecture itself is “designed to scale only when needed” rather than requiring premature optimization.&lt;/p&gt;
&lt;p&gt;Some applications have special requirements that framework-defined infrastructure doesn’t handle well. That’s fine. Those applications can opt out and configure infrastructure manually. But for the majority of web applications, the standard patterns that frameworks encode work perfectly, and automating their infrastructure deployment makes obvious economic sense.&lt;/p&gt;
&lt;h2 id=&quot;what-this-means-for-the-industry&quot;&gt;What This Means for the Industry&lt;a class=&quot;heading-anchor&quot; href=&quot;#what-this-means-for-the-industry&quot; aria-label=&quot;Permalink to What This Means for the Industry&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Framework-defined infrastructure represents a maturation of the platform-as-a-service model. Early PaaS offerings like Heroku abstracted infrastructure but required you to build against their specific deployment model. Modern framework-defined infrastructure abstracts infrastructure while letting you build against open source frameworks.&lt;/p&gt;
&lt;p&gt;This distinction matters. When the abstraction layer is proprietary, adoption requires trust in a single vendor. When the abstraction layer is an open source framework, adoption requires trust in the broader ecosystem. The latter scales better.&lt;/p&gt;
&lt;p&gt;This is reflected in what Vercel calls their &lt;a href=&quot;https://vercel.com/blog/open-sdk-strategy&quot;&gt;Open SDK strategy&lt;/a&gt;: “We want what we build to work extremely well on Vercel, but not at the cost of lock-in… We will build first on Vercel, where we can iterate fastest to ensure the best end-to-end developer and user experience. And as we become confident projects are mature, we’ll invest in ensuring our SDKs and tools are deployable to any platform.”&lt;/p&gt;
&lt;p&gt;Similarly, Netlify and Cloudflare have &lt;a href=&quot;https://www.netlify.com/blog/supporting-an-open-web-with-netlify-cloudflare/&quot;&gt;come together to support open frameworks&lt;/a&gt; like TanStack and Astro, recognizing that “a rising tide lifts all boats.” As they note, “the strongest projects emerge when multiple organizations align behind shared goals, giving developers the broadest opportunities for success.”&lt;/p&gt;
&lt;p&gt;We should expect framework-defined infrastructure to become the default deployment model for framework-based applications, just as cloud computing became the default infrastructure model for most applications. Some organizations will continue self-hosting and managing infrastructure directly, just as some organizations still run their own data centers. But the center of gravity will shift.&lt;/p&gt;
&lt;h2 id=&quot;the-evolution-continues&quot;&gt;The Evolution Continues&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-evolution-continues&quot; aria-label=&quot;Permalink to The Evolution Continues&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The pattern is already extending beyond basic deployment. Vercel’s recent introduction of their &lt;a href=&quot;https://vercel.com/blog/introducing-workflow&quot;&gt;Workflow Development Kit&lt;/a&gt; shows how framework-defined infrastructure concepts apply to durability and long-running processes: “Functions can pause for minutes or months, survive deployments and crashes, and resume exactly where they stopped.” Rather than manually configuring message queues and retry logic, developers write standard async functions with durability directives, and &lt;a href=&quot;https://vercel.com/blog/introducing-workflow&quot;&gt;the platform handles persistence automatically&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Similarly, their &lt;a href=&quot;https://vercel.com/blog/self-driving-infrastructure&quot;&gt;self-driving infrastructure&lt;/a&gt; initiative explores how production data can inform infrastructure optimization: “Code defines infrastructure, production informs code, and infrastructure adapts automatically.” This closes the loop between application behavior and resource allocation without requiring manual intervention.&lt;/p&gt;
&lt;p&gt;Vercel’s approach to &lt;a href=&quot;https://vercel.com/blog/life-of-a-request-application-aware-routing&quot;&gt;application-aware routing&lt;/a&gt; demonstrates how deeply framework understanding can integrate with infrastructure: “Vercel uses your framework code to define infrastructure and how requests are handled at runtime. This tight integration gives the platform full visibility into your app’s structure: routes, layouts, rewrites, middleware, functions, static assets, and routing logic.”&lt;/p&gt;
&lt;h2 id=&quot;conclusion-abstraction-is-inevitable&quot;&gt;Conclusion: Abstraction is Inevitable&lt;a class=&quot;heading-anchor&quot; href=&quot;#conclusion-abstraction-is-inevitable&quot; aria-label=&quot;Permalink to Conclusion: Abstraction is Inevitable&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Software development has always moved toward better abstractions. We abstract when the benefits of simplicity outweigh the costs of lost control, when patterns emerge that are stable enough to codify, when the manual work no longer creates competitive advantage.&lt;/p&gt;
&lt;p&gt;Framework-defined infrastructure meets all these criteria. The patterns are stable, server-side rendering, static generation, API routes, and edge middleware aren’t going anywhere. The manual work, translating framework concepts to infrastructure primitives, creates no competitive advantage. The simplification, automatic infrastructure from framework code, delivers real value.&lt;/p&gt;
&lt;p&gt;As &lt;a href=&quot;https://blog.heroku.com/modern-web-app-architecture&quot;&gt;Heroku’s evolution demonstrates&lt;/a&gt;, “ever-increasing complexity means developers need to build and operate more adaptable systems.” Framework-defined infrastructure represents exactly this kind of adaptability: composing infrastructure from framework patterns rather than manual configuration.&lt;/p&gt;
&lt;p&gt;The question isn’t whether framework-defined infrastructure will become standard practice, but how quickly it will happen. Given that multiple platforms have independently adopted similar approaches, given that major companies successfully deploy framework-based applications across different infrastructure models, and given the clear economic benefits, the trajectory seems clear.&lt;/p&gt;
&lt;p&gt;Abstractions that work become invisible. In a few years, explaining that you used to manually configure infrastructure for every framework route will sound as strange as explaining that you used to manually manage memory allocation. We’ll wonder why we ever did it differently.&lt;/p&gt;
</content:encoded></item><item><title>How to Sync Apple Reminders with Android (When You&apos;re Hooked on the Menubar)</title><link>https://afonsojramos.me/blog/todo-sync-with-apple-reminders-and-outlook/</link><guid isPermaLink="true">https://afonsojramos.me/blog/todo-sync-with-apple-reminders-and-outlook/</guid><description>Learn how to sync Apple Reminders with Android using Microsoft as a bridge solution. Perfect for users committed to reminders-menubar on Mac who need occasional Android access. Includes setup steps, limitations, and honest assessment of sync delays and notification quirks.</description><pubDate>Tue, 23 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you’re like me and have discovered &lt;a href=&quot;https://github.com/DamascenoRafael/reminders-menubar&quot;&gt;&lt;code&gt;reminders-menubar&lt;/code&gt;&lt;/a&gt;, you know exactly why leaving Apple Reminders isn’t an option. This open-source menubar app transforms Apple’s basic reminder system into something genuinely great - instant access with a keyboard shortcut, natural language input, and that satisfying feeling of capturing tasks without breaking your flow. And I’ve looked at other options, but nothing else comes close to the seamlessness of reminders-menubar on macOS, even if you’re willing to pay (and I’m not).&lt;/p&gt;
&lt;p&gt;The problem? I also use an Android phone. And Apple Reminders doesn’t exactly play nice outside the walled garden.&lt;/p&gt;
&lt;p&gt;So here’s my situation: I’m completely sold on the workflow that reminders-menubar provides on my Mac. It’s become part of how I think about task management - quick capture, always visible and with number that I hold myself accountable to. But when I pick up my Android phone, those reminders might as well not exist, or I might need to mimic them in another app - which was Google Keep for a while, but I kept forgetting to check it.&lt;/p&gt;
&lt;p&gt;Fair warning, the solution I’m about to share isn’t perfect, as it does involve having a middleman, Microsoft, but after you try the &lt;code&gt;reminders-menubar&lt;/code&gt; workflow, I’m pretty confident you’ll embrace it, like I have, as this is currently your best option when needing Android access.&lt;/p&gt;
&lt;nav class=&quot;table-of-contents&quot;&gt;&lt;ol&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#why-reminders-menubar-changes-everything&quot; title=&quot;Why Reminders-Menubar Changes Everything&quot;&gt;Why Reminders-Menubar Changes Everything&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-microsoft-bridge-solution&quot; title=&quot;The Microsoft Bridge Solution&quot;&gt;The Microsoft Bridge Solution&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#setting-it-up&quot; title=&quot;Setting It Up&quot;&gt;Setting It Up&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-only-downside-is-actually-an-upside&quot; title=&quot;The Only Downside Is Actually An Upside&quot;&gt;The Only Downside Is Actually An Upside&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-bigger-picture&quot; title=&quot;The Bigger Picture&quot;&gt;The Bigger Picture&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/nav&gt;&lt;h2 id=&quot;why-reminders-menubar-changes-everything&quot;&gt;Why Reminders-Menubar Changes Everything&lt;a class=&quot;heading-anchor&quot; href=&quot;#why-reminders-menubar-changes-everything&quot; aria-label=&quot;Permalink to Why Reminders-Menubar Changes Everything&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;But before we dive into the Android sync solution, let me explain why I’m going through this trouble instead of just switching to multi-platform tool such as Todoist or TickTick.&lt;/p&gt;
&lt;p&gt;Reminders-menubar is a lightweight menubar Mac app that expands on Apple Reminders by gives you instant access to it through the menubar, or a quick keyboard shortcut. No opening apps, no clicking through menus, no friction.&lt;/p&gt;
&lt;p&gt;The magic is that it uses Apple’s native EventKit framework, so everything syncs perfectly with iCloud. Your reminders appear instantly on your iPhone, iPad, and Apple Watch (even if in my case I only have my Mac). At the end of the day, it’s free, it’s fast, and it just works. Which is exactly why I don’t want to give it up just so I can have the same ToDos on my phone. I tend to avoid paid subscriptions when free alternatives exist, even if they aren’t quite as polished and require a bit more tinkering.&lt;/p&gt;
&lt;h2 id=&quot;the-microsoft-bridge-solution&quot;&gt;The Microsoft Bridge Solution&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-microsoft-bridge-solution&quot; aria-label=&quot;Permalink to The Microsoft Bridge Solution&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here’s the workaround: it is actually quite simple, we’re going to use Microsoft’s ecosystem to bridge Apple and Android. Microsoft Outlook can sync with Apple Reminders on iOS, and Microsoft To Do can access those same reminders on Android. Plus Microsoft To Do has a pretty nice widget.&lt;/p&gt;
&lt;h2 id=&quot;setting-it-up&quot;&gt;Setting It Up&lt;a class=&quot;heading-anchor&quot; href=&quot;#setting-it-up&quot; aria-label=&quot;Permalink to Setting It Up&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The setup is surprisingly straightforward, even if the underlying architecture feels a bit convoluted.&lt;/p&gt;
&lt;h4 id=&quot;macos-setup&quot;&gt;macOS Setup&lt;a class=&quot;heading-anchor&quot; href=&quot;#macos-setup&quot; aria-label=&quot;Permalink to macOS Setup&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;First, we need to configure your Mac to sync Apple Reminders with Microsoft:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open macOS System Settings → Internet Accounts&lt;/li&gt;
&lt;li&gt;Add your Microsoft account if it’s not already there&lt;/li&gt;
&lt;li&gt;Make sure “Reminders” is checked in the account services&lt;/li&gt;
&lt;li&gt;That’s it! Nothing else to configure here.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;ios-setup&quot;&gt;iOS Setup&lt;a class=&quot;heading-anchor&quot; href=&quot;#ios-setup&quot; aria-label=&quot;Permalink to iOS Setup&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;On your iPhone or iPad:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open iOS Settings → Apps → Reminders&lt;/li&gt;
&lt;li&gt;Tap Reminders Accounts → Add Account&lt;/li&gt;
&lt;li&gt;Add your Microsoft Outlook account&lt;/li&gt;
&lt;li&gt;Ensure the “Reminders” toggle is ON&lt;/li&gt;
&lt;li&gt;That’s it! Nothing else to configure here.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This creates the bridge so that reminders created through &lt;code&gt;reminders-menubar&lt;/code&gt; (or the native Reminders app) sync to Microsoft’s servers.&lt;/p&gt;
&lt;h4 id=&quot;on-your-android-device&quot;&gt;On Your Android Device&lt;a class=&quot;heading-anchor&quot; href=&quot;#on-your-android-device&quot; aria-label=&quot;Permalink to On Your Android Device&quot;&gt;#&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Install Microsoft To Do from the Play Store and sign in with the same Microsoft account. That’s it. Your reminders should start appearing shortly. And since you’re using Microsoft To Do, you can actually use other platforms that integrate with it, such as Samsung’s native Reminder app.&lt;/p&gt;
&lt;h2 id=&quot;the-only-downside-is-actually-an-upside&quot;&gt;The Only Downside Is Actually An Upside&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-only-downside-is-actually-an-upside&quot; aria-label=&quot;Permalink to The Only Downside Is Actually An Upside&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here’s the one quirk you should know about: when a reminder’s alarm fires, it rings on BOTH devices. And dismissing it on one device doesn’t dismiss it on the other.&lt;/p&gt;
&lt;p&gt;Set a reminder for 3 PM, and at 3 PM both your iPhone and Android buzz. Dismiss it on Android, and your iPhone keeps nagging. This is particularly noticeable with recurring reminders (not that I have daily reminders that bug me on multiple devices simultaneously 🫣, but still).&lt;/p&gt;
&lt;p&gt;This happens because each platform maintains its own notification state. The reminder data syncs perfectly, but the “I’ve handled this” status doesn’t.&lt;/p&gt;
&lt;p&gt;But here’s the thing - I’ve actually started to appreciate this “bug” as a feature. It’s pretty hard to miss an important reminder when two devices are insisting you pay attention to it. Now, I hear you, notifications can be annoying. But for truly important deadlines or time-sensitive tasks, having that redundant notification system isn’t the worst thing in the world - we’re talking about reminders, after all.&lt;/p&gt;
&lt;h2 id=&quot;the-bigger-picture&quot;&gt;The Bigger Picture&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-bigger-picture&quot; aria-label=&quot;Permalink to The Bigger Picture&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;What strikes me about this whole situation is how it highlights the state of cross-platform compatibility in 2025. We’re still using workarounds and compromises to make basic productivity tools work across ecosystems.&lt;/p&gt;
&lt;p&gt;Apple’s walled garden provides genuine benefits - the integration really is excellent when you stay inside it. But it punishes flexibility. The fact that a free, open-source Mac app (reminders-menubar) can make Apple’s basic reminder system more compelling than premium cross-platform alternatives says something about the value of deep platform integration, as well as the imense capabilities of open-source software.&lt;/p&gt;
&lt;p&gt;Microsoft, ironically, has positioned themselves as the peace broker by building quality apps on both platforms and enabling sync between them. For now, if you need Apple Reminders on Android like me - especially if reminders-menubar is part of why you’re committed to Apple’s ecosystem - this Microsoft bridge approach is your best bet.&lt;/p&gt;
&lt;p&gt;Is it elegant? No. Does it preserve the workflow that makes reminders-menubar so valuable? Absolutely. And honestly, once you get it set up, you’ll forget about the Microsoft middleman entirely - it just works seamlessly in the background while you focus on actually getting things done.&lt;/p&gt;
</content:encoded></item><item><title>Hono vs Elysia for Astro on Cloudflare: What I Chose</title><link>https://afonsojramos.me/blog/elysia-vs-hono-astro-cloudflare/</link><guid isPermaLink="true">https://afonsojramos.me/blog/elysia-vs-hono-astro-cloudflare/</guid><description>A practical Hono vs Elysia comparison for Astro API routes on Cloudflare, including the new Elysia Cloudflare adapter and why I chose Hono.</description><pubDate>Tue, 11 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When building an Astro application, one of the first decisions you’ll face is how to handle API routes. If you value type safety and developer experience, you may have considered Elysia or Hono, two popular TypeScript web frameworks.&lt;/p&gt;
&lt;p&gt;I recently went through the process of migrating (on the same day 😅) from &lt;a href=&quot;https://elysiajs.com/&quot;&gt;Elysia&lt;/a&gt; to &lt;a href=&quot;https://hono.dev/&quot;&gt;Hono&lt;/a&gt; for my personal website, and I wanted to share my experience, particularly when deploying to Cloudflare Pages and dealing with environment variables.&lt;/p&gt;
&lt;nav class=&quot;table-of-contents&quot;&gt;&lt;ol&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#hono-vs-elysia-the-short-answer&quot; title=&quot;Hono vs Elysia: The Short Answer&quot;&gt;Hono vs Elysia: The Short Answer&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-initial-setup-elysia&quot; title=&quot;The Initial Setup: Elysia&quot;&gt;The Initial Setup: Elysia&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#the-cloudflare-challenge-in-2025&quot; title=&quot;The Cloudflare Challenge in 2025&quot;&gt;The Cloudflare Challenge in 2025&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#enter-hono-the-solution&quot; title=&quot;Enter Hono: The Solution&quot;&gt;Enter Hono: The Solution&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#refactoring-the-api-handlers&quot; title=&quot;Refactoring the API Handlers&quot;&gt;Refactoring the API Handlers&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#updated-hono-vs-elysia-results-july-2026&quot; title=&quot;Updated Hono vs Elysia Results (July 2026)&quot;&gt;Updated Hono vs Elysia Results (July 2026)&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#conclusion&quot; title=&quot;Conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/nav&gt;&lt;h2 id=&quot;hono-vs-elysia-the-short-answer&quot;&gt;Hono vs Elysia: The Short Answer&lt;a class=&quot;heading-anchor&quot; href=&quot;#hono-vs-elysia-the-short-answer&quot; aria-label=&quot;Permalink to Hono vs Elysia: The Short Answer&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For Astro API routes deployed to Cloudflare, &lt;strong&gt;I chose Hono&lt;/strong&gt;. Both frameworks handled my routes well in development, but Hono let me pass Cloudflare environment bindings from Astro’s runtime context directly into the app. At the time of this migration, I could not find an equally clean way to do that with Elysia’s request handler.&lt;/p&gt;
&lt;aside class=&quot;callout callout-note&quot; role=&quot;note&quot;&gt;
&lt;div class=&quot;callout-title&quot;&gt;July 2026 update&lt;/div&gt;
&lt;p&gt;Elysia now has an &lt;a href=&quot;https://elysiajs.com/integrations/cloudflare-worker&quot;&gt;experimental Cloudflare Worker adapter&lt;/a&gt;. Its documentation shows support for Cloudflare bindings through &lt;code&gt;cloudflare:workers&lt;/code&gt;, requires a compatibility date of at least &lt;code&gt;2025-06-01&lt;/code&gt;, and lists several current limitations. That option was not available when I wrote this article in March 2025. If I were choosing today, I would test the adapter against my Astro integration before deciding.&lt;/p&gt;
&lt;/aside&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Elysia&lt;/th&gt;
&lt;th&gt;Hono&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Astro endpoint handler&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app.handle(request)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app.fetch(request, env)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare bindings&lt;/td&gt;
&lt;td&gt;Available through the experimental Worker adapter&lt;/td&gt;
&lt;td&gt;Available through &lt;code&gt;c.env&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;July 2026 dry-run build&lt;/td&gt;
&lt;td&gt;155.03 KiB gzip&lt;/td&gt;
&lt;td&gt;15.39 KiB gzip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;My existing Astro setup&lt;/td&gt;
&lt;td&gt;Would require retesting the adapter or changing how bindings are read&lt;/td&gt;
&lt;td&gt;Already deployed and working&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is not a universal benchmark or a claim that Hono is always better. It is a comparison of the integration problem I encountered: getting Cloudflare bindings into API handlers while keeping the Astro deployment simple.&lt;/p&gt;
&lt;h2 id=&quot;the-initial-setup-elysia&quot;&gt;The Initial Setup: Elysia&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-initial-setup-elysia&quot; aria-label=&quot;Permalink to The Initial Setup: Elysia&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Let me be honest upfront - I didn’t strictly &lt;em&gt;need&lt;/em&gt; a framework for my simple API routes. Astro’s built-in API functionality would have been perfectly adequate for my use case. But as developers, we often choose technologies not just for practical reasons, but also for the sake of experimentation and learning. I wanted to try something new and see what these Bun-optimized frameworks had to offer.&lt;/p&gt;
&lt;p&gt;My journey began with Elysia, a framework that promises “sub-millisecond” performance and a delightful developer experience. Setting up API routes in Astro with Elysia is straightforward:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { getTopTracks } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;~/api/lastfm&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { APIRoute } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;astro&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { Elysia } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;elysia&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; app&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Elysia&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ prefix: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;/api&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, aot: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;app.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;/top-tracks&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, getTopTracks);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; handle&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; APIRoute&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ({ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;request&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; app.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;handle&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(request);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; GET&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; handle;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; POST&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; handle;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This worked perfectly in development. The API routes were fast, type-safe, and the developer experience was excellent. However, when I deployed to Cloudflare Pages, I encountered a significant limitation.&lt;/p&gt;
&lt;h2 id=&quot;the-cloudflare-challenge-in-2025&quot;&gt;The Cloudflare Challenge in 2025&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-cloudflare-challenge-in-2025&quot; aria-label=&quot;Permalink to The Cloudflare Challenge in 2025&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Cloudflare Pages uses a runtime environment that differs from your local development setup. One key difference is how environment variables are accessed. In Cloudflare, environment variables are not directly available through &lt;code&gt;process.env&lt;/code&gt; but are instead passed through the request context, which means that it is not globally available like in your local development environment.&lt;/p&gt;
&lt;p&gt;With Elysia, I couldn’t find a clean way to access the Cloudflare runtime context and pass it to my API handlers. My Last.fm API key was stored as a Cloudflare secret, which then is available as an environment variable, but my handlers couldn’t access it because Elysia’s &lt;code&gt;handle&lt;/code&gt; method only accepted the request object, not the full Astro context. This describes the integration I tested in March 2025, not Elysia’s current Cloudflare support.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; handle&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; APIRoute&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ({ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;request&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;locals&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // No way to pass locals.runtime.env to Elysia handlers 😭&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; app.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;handle&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(request);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;enter-hono-the-solution&quot;&gt;Enter Hono: The Solution&lt;a class=&quot;heading-anchor&quot; href=&quot;#enter-hono-the-solution&quot; aria-label=&quot;Permalink to Enter Hono: The Solution&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;So, if Elysia was out of the question, I needed to find a new framework that could handle the Cloudflare runtime context. And who’s the second most popular framework after Elysia? Hono! And to be honest, Hono is becoming a way more exciting framework with a ton of built-in features.&lt;/p&gt;
&lt;p&gt;Now, being aware of the problem at hand, I was quick to discover that Hono does offer a more flexible approach to handling contexts.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// [...slugs].ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { getNowPlaying, getTopTracks } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;~/api/lastfm&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { APIRoute } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;astro&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { Hono, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Context &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; HonoContext } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;hono&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { BlankInput } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;hono/types&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Bindings&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;  LASTFM_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; interface&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Context&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; extends&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; HonoContext&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;lt;{ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;Bindings&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Bindings&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;BlankInput&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;gt; {}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; app&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Hono&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;lt;{ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;Bindings&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Bindings&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }&amp;gt;().&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;basePath&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;/api&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;app&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  .&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;/now-playing&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;c&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; getNowPlaying&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(c))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  .&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;/top-tracks&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;c&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; getTopTracks&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(c));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; handle&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; APIRoute&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ({ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;request&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;locals&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // To get the locals.runtime object correctly typed, we need to first follow the guide: https://docs.astro.build/en/guides/integrations-guide/cloudflare/#typing&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // Then we can pass the environment variables to the Hono app&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  app.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;fetch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(request, { &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;locals.runtime.env });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; GET&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; handle;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; POST&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; handle;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key difference is that Hono’s &lt;code&gt;fetch&lt;/code&gt; method accepts a second parameter where you can pass environment variables and other context. This allowed me to pass &lt;code&gt;locals.runtime.env&lt;/code&gt; from Astro’s context directly to my Hono app, making those environment variables available to my API handlers.&lt;/p&gt;
&lt;h2 id=&quot;refactoring-the-api-handlers&quot;&gt;Refactoring the API Handlers&lt;a class=&quot;heading-anchor&quot; href=&quot;#refactoring-the-api-handlers&quot; aria-label=&quot;Permalink to Refactoring the API Handlers&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;With this change, I also needed to update my API handlers to accept the Hono context:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Before with Elysia&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; getTopTracks&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; () &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; API_KEY&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; process.env.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;LASTFM_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // Rest of the handler&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// After with Hono&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; getTopTracks&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;c&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Context&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; API_KEY&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; c.env.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;LASTFM_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // Rest of the handler&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach has several advantages:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Improved type safety for environment variables&lt;/li&gt;
&lt;li&gt;Access to Cloudflare-specific features&lt;/li&gt;
&lt;li&gt;Cleaner separation of concerns&lt;/li&gt;
&lt;li&gt;Better testability since dependencies are injected (not that I’m going to write tests for my personal website 🫣, but still)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Is my codebase heavily tied to Cloudflare Pages? A bit, but I’m okay with that. Moving from it, if I were to move away from the amazing Cloudflare experience that I’ve been enjoying, would still be easier than the migration from NextJS (with CSS Modules) to Astro.&lt;/p&gt;
&lt;h2 id=&quot;updated-hono-vs-elysia-results-july-2026&quot;&gt;Updated Hono vs Elysia Results (July 2026)&lt;a class=&quot;heading-anchor&quot; href=&quot;#updated-hono-vs-elysia-results-july-2026&quot; aria-label=&quot;Permalink to Updated Hono vs Elysia Results (July 2026)&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I retested with Elysia 1.4.29, Hono 4.12.30, Node 24.18.0, Bun 1.3.14, and Wrangler 4.110.0 on an Apple Silicon Mac. Both Cloudflare Worker implementations exposed the same dynamic text route and read the same environment binding.&lt;/p&gt;
&lt;p&gt;First, the result that matters most for this article: &lt;strong&gt;both frameworks now compile successfully for Cloudflare Workers with environment bindings&lt;/strong&gt;. The Elysia build used its experimental &lt;code&gt;CloudflareAdapter&lt;/code&gt; and &lt;code&gt;cloudflare:workers&lt;/code&gt;; the Hono build used its documented &lt;a href=&quot;https://hono.dev/docs/getting-started/cloudflare-workers#bindings&quot;&gt;&lt;code&gt;c.env&lt;/code&gt; binding API&lt;/a&gt;. Wrangler’s dry-run output produced these bundle sizes:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th style=&quot;text-align: right&quot;&gt;Gzip bundle&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Elysia&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;155.03 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hono&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;15.39 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I also ran a small handler-dispatch benchmark: 25,000 warm-up requests followed by seven runs of 250,000 requests to a dynamic &lt;code&gt;hello/:name&lt;/code&gt; route. These are the median results:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th style=&quot;text-align: right&quot;&gt;Elysia requests/second&lt;/th&gt;
&lt;th style=&quot;text-align: right&quot;&gt;Hono requests/second&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;653,565&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;686,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bun&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;5,047,077&lt;/td&gt;
&lt;td style=&quot;text-align: right&quot;&gt;1,205,095&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Treat those numbers as a framework dispatch microbenchmark, not as production throughput. It excludes Astro, the network, Cloudflare’s runtime, cold starts, and application work. The useful conclusion is that both are fast enough for my tiny API, Elysia benefits enormously from Bun, and Hono produced a much smaller Cloudflare bundle in this test.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;a class=&quot;heading-anchor&quot; href=&quot;#conclusion&quot; aria-label=&quot;Permalink to Conclusion&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The migration from Elysia to Hono was relatively painless and solved my specific issue with accessing environment variables in Cloudflare Pages. Today, both frameworks have documented Astro and Cloudflare paths, so the limitation that triggered my migration is no longer a fair summary of Elysia as a whole.&lt;/p&gt;
&lt;p&gt;If you’re building an Astro site with a handful of API routes, start by asking whether Astro’s built-in endpoints are enough. If you do want a framework, Hono remains my default for Astro on Cloudflare because its binding model is direct, its Worker integration is mature, and it produced the smaller bundle here. Elysia is now a credible option too, especially when Bun performance and its end-to-end type system matter more to you, but its Cloudflare adapter is still documented as experimental.&lt;/p&gt;
&lt;p&gt;Sometimes the best technical decisions come from experimentation rather than strict necessity. And I do like to experiment with new technologies, so I’m glad I went through this experience.&lt;/p&gt;
&lt;p&gt;As a closing note, I’m extremely bullish on &lt;a href=&quot;https://bun.sh/&quot;&gt;Bun&lt;/a&gt; as a JavaScript runtime. Its built-in HTTP server capabilities are becoming increasingly powerful with each release, potentially making it a compelling alternative to dedicated frameworks like Elysia or Hono (or even Vite for that matter) for certain use cases. As Bun continues to mature, we might find ourselves reaching for these frameworks less often, especially for simpler API routes. That said, the ecosystem around Bun is thriving precisely because of innovative frameworks like these, and I’m excited to see how they all evolve together.&lt;/p&gt;
&lt;aside class=&quot;callout callout-note&quot; role=&quot;note&quot;&gt;
&lt;div class=&quot;callout-title&quot;&gt;July 2026 update&lt;/div&gt;
&lt;p&gt;I’m less bullish today. I still use Bun in production and value what it gets right, but I have also encountered memory leaks in long-running workloads. Bun’s &lt;a href=&quot;https://bun.com/blog/bun-in-rust&quot;&gt;account of its rewrite from Zig to Rust&lt;/a&gt; openly describes the memory leaks and crashes that motivated the change. &lt;a href=&quot;https://andrewkelley.me/post/my-thoughts-bun-rust-rewrite.html&quot;&gt;Zig creator Andrew Kelley disputed Bun creator Jarred Sumner’s framing&lt;/a&gt;, arguing that the deeper problem was Bun’s engineering practices and accumulated technical debt rather than Zig itself; the public exchange also became more personal than the technical disagreement warranted. I don’t see Bun’s problems as an indictment of Zig, nor do I assume that changing languages automatically fixes the underlying engineering trade-offs. Taken together with my own production experience, the episode reduced my trust in Bun as a runtime for long-lived services. It remains an impressive project that I will continue to use where it fits, but with more caution than this original closing note suggests.&lt;/p&gt;
&lt;/aside&gt;
</content:encoded></item><item><title>Optimising prompt engineering for better AI outputs</title><link>https://afonsojramos.me/blog/prompt-engineering/</link><guid isPermaLink="true">https://afonsojramos.me/blog/prompt-engineering/</guid><description>Learn how well-crafted prompts improve AI performance, user experience, and efficiency.</description><pubDate>Mon, 03 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;aside class=&quot;callout callout-note&quot; role=&quot;note&quot;&gt;
&lt;div class=&quot;callout-title&quot;&gt;Also published elsewhere&lt;/div&gt;
&lt;p&gt;This article was also published on the &lt;strong&gt;CNCF blog&lt;/strong&gt; &lt;a href=&quot;https://www.cncf.io/blog/2025/01/03/optimising-prompt-engineering-for-better-ai-outputs/&quot;&gt;here&lt;/a&gt; and on the &lt;strong&gt;YLD blog&lt;/strong&gt; &lt;a href=&quot;https://www.yld.io/blog/optimising-prompt-engineering-for-better-ai-outputs&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/aside&gt;
&lt;p&gt;Remember when searching for information online involved typing in a few keywords and sifting through pages of results? Thankfully, those days are long gone.&lt;/p&gt;
&lt;p&gt;Today’s search engines have transformed the way we find information online. From simple keyword matching to advanced technologies like semantic search and natural language processing, search engines have come a long way. But, have you ever stopped to consider the UX design choices that make these search experiences possible?&lt;/p&gt;
&lt;p&gt;An effective UX/ search experience design involves a deep understanding of how humans interact with machines and the ability to adapt to that relationship. It’s not an innate skill, but rather something that can be developed over time, much like the act of googling is a skill that many of us have honed through years of practice. Similarly, interacting with Generative AI models requires a certain level of skill and knowledge which involves Prompt Engineering.&lt;/p&gt;
&lt;p&gt;This article aims to provide a comprehensive guide to Prompt Engineering, helping you understand its importance, techniques, and best practices. You’ll be equipped to create effective prompts that maximise the potential of AI models and enhance user experiences.&lt;/p&gt;
&lt;nav class=&quot;table-of-contents&quot;&gt;&lt;ol&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#why-is-prompt-engineering-important&quot; title=&quot;Why is Prompt Engineering important?&quot;&gt;Why is Prompt Engineering important?&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#precision-in-prompt-engineering&quot; title=&quot;Precision in Prompt Engineering&quot;&gt;Precision in Prompt Engineering&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#are-all-prompts-equal&quot; title=&quot;Are all prompts equal?&quot;&gt;Are all prompts equal?&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#writing-your-best-prompt&quot; title=&quot;Writing your best prompt&quot;&gt;Writing your best prompt&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#closing-thoughts&quot; title=&quot;Closing thoughts&quot;&gt;Closing thoughts&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/nav&gt;&lt;h2 id=&quot;why-is-prompt-engineering-important&quot;&gt;Why is Prompt Engineering important?&lt;a class=&quot;heading-anchor&quot; href=&quot;#why-is-prompt-engineering-important&quot; aria-label=&quot;Permalink to Why is Prompt Engineering important?&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Well-crafted prompts are essential for maximising the potential of AI models, particularly LLMs. They enhance AI performance by providing clear instructions and context, leading to more accurate and relevant responses.&lt;/p&gt;
&lt;p&gt;Prompt Engineering improves user experience by making interactions more intuitive and reducing ambiguity, minimising the risk of misinterpretation. It enables AI models to handle complex tasks, adapt to different use cases, and ensure consistency in outputs, which is vital for integrated systems.&lt;/p&gt;
&lt;h2 id=&quot;precision-in-prompt-engineering&quot;&gt;Precision in Prompt Engineering&lt;a class=&quot;heading-anchor&quot; href=&quot;#precision-in-prompt-engineering&quot; aria-label=&quot;Permalink to Precision in Prompt Engineering&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Prompt Engineering is a discipline that involves developing and optimising prompts to efficiently use language models for a wide variety of applications and research topics. This discipline is particularly useful for developers, researchers, and anyone looking to leverage AI models for various applications and research topics. It encompasses various skills and techniques essential for interacting with and developing LLMs. Mastering this discipline enables you to optimise these interactions, achieving more accurate and relevant outcomes.&lt;/p&gt;
&lt;p&gt;A prompt refers to a statement or question that is employed to trigger a response from a language model or other AI system. Prompts are generally crafted to offer context or instructions to the AI model, directing it to produce a specific type of output or carry out a particular task. A prompt can be provided to the language model by the user or by the system itself, serving as a means to define its default behaviour.&lt;/p&gt;
&lt;p&gt;Creating effective prompts requires a deep understanding of both the AI model (various models respond uniquely to different types of prompts) and the user’s intent. Just as search engines use algorithms to understand the user’s query and return relevant results, Prompt Engineering involves designing prompts to communicate the user’s intent to the AI model.&lt;/p&gt;
&lt;p&gt;Experimenting with different formats, testing various instructions and contexts, and refining the prompt based on the AI model’s responses are key to creating prompts that produce the desired response from the AI model while minimising the risk of misinterpretation or ambiguity.&lt;/p&gt;
&lt;p&gt;The images below demonstrate how slight prompt variations can lead to very different results. However, consistency is crucial in integrated systems. A good prompt aims to produce repeatable outputs with minimal variation, ensuring reliable and predictable AI performance.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/chatgpt-spaghetti-made-of.-2kdkv-a_1miahn.webp&quot; alt=&quot;Screenshot of the first prompt: “What is spaghetti made of?”&quot;&gt;&lt;figcaption&gt;Screenshot of the first prompt: “What is spaghetti made of?”&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p class=&quot;text-center italic&quot;&gt;
  Screenshot of the first prompt: “What is spaghetti made of?”
&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/chatgpt-spaghetti.BCur8iEt_Z1JB7OO.webp&quot; alt=&quot;Screenshot of the second prompt with a slight variation: ”What is spaghetti?”&quot;&gt;&lt;figcaption&gt;Screenshot of the second prompt with a slight variation: ”What is spaghetti?”&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p class=&quot;text-center italic&quot;&gt;
  Screenshot of the second prompt with a slight variation: ”What is spaghetti?”
&lt;/p&gt;
&lt;p&gt;When it comes to the daily interaction with an LLM, a varied yet similar result would be acceptable. But, when it comes to using an LLM as part of an integrated system, consistency is an important factor. What makes a prompt “good” is about producing a somewhat repeatable output with minimal variation.&lt;/p&gt;
&lt;h2 id=&quot;are-all-prompts-equal&quot;&gt;Are all prompts equal?&lt;a class=&quot;heading-anchor&quot; href=&quot;#are-all-prompts-equal&quot; aria-label=&quot;Permalink to Are all prompts equal?&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;No. Not all prompts are created equally because different types of prompts serve different purposes and can significantly impact the quality and relevance of the AI model’s responses. Understanding the various types of prompts and their applications is key to effective Prompt Engineering.&lt;/p&gt;
&lt;p&gt;As a way to scientifically define methods of communication with LLMs, many have tried to create both techniques and frameworks that systematically define how to write these prompts.&lt;/p&gt;
&lt;p&gt;Here are some common types of AI prompts that serve unique purposes:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One-Shot and Few-Shot prompts&lt;/strong&gt;: If you want to follow a structure for a JIRA ticket, providing a well-structured template will help the AI generate similar ones. Few-shot or One-Shot prompts both help the AI adapt quickly to new tasks by providing a small set of examples, enhancing its ability to generate relevant and accurate responses. These prompting techniques involve providing the AI with examples of the desired task or output before asking it to complete a similar task. By showing the model what is expected through one or a few examples, the AI learns the context and format needed and applies it to new inputs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zero-Shot prompts&lt;/strong&gt;: A practical use case is asking the AI to translate a sentence from English to French without providing any examples; the AI must rely on its pre-training to understand and perform the task. This approach is particularly useful for assessing how well the AI can handle novel or unexpected queries, demonstrating its ability to apply learned patterns to new contexts. Unlike few-shot prompts, zero-shot prompts require the AI to perform tasks without prior examples, relying solely on its pre-training. This approach is valuable for evaluating the AI’s adaptability and versatility.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Chain-of-Thought prompts&lt;/strong&gt;: These prompts guide the AI to follow a logical progression or reasoning pathway to reach a conclusion or solve a problem. The prompt encourages the AI to detail its
Each of these Prompt Engineering techniques can be adapted and combined depending on the specific requirements of the task at hand and the capabilities of the AI model being used.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hybrid prompts&lt;/strong&gt;: Combining multiple techniques, hybrid prompts might integrate direct instructions with creative challenges or conditional elements with exploratory questions to guide the AI more effectively according to complex needs. For example, you might provide the AI with a Few-Shot prompt to understand the structure of a report, then add a Chain-of-Thought prompt to ensure it details its reasoning, and finally include a Meta-prompt to ask the AI to reflect on its approach. Hybrid prompts are versatile and can be tailored to meet the specific requirements of various tasks, making them particularly useful for complex and multi-faceted projects.&lt;/p&gt;
&lt;p&gt;Effective Prompt Engineering involves a deep understanding of these techniques and the ability to apply them creatively. By leveraging the right prompt type, you can significantly enhance the AI model’s performance and the overall user experience. If you want to read more about the several types of prompts, check out &lt;a href=&quot;https://www.promptingguide.ai/techniques&quot;&gt;Prompting Guide’s techniques page&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;writing-your-best-prompt&quot;&gt;Writing your best prompt&lt;a class=&quot;heading-anchor&quot; href=&quot;#writing-your-best-prompt&quot; aria-label=&quot;Permalink to Writing your best prompt&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Crafting the perfect prompt is like giving directions to a slightly distracted, incredibly smart friend - you need to be clear, concise, and maybe even a little clever.&lt;/p&gt;
&lt;p&gt;Start with a basic prompt and refine it iteratively based on the responses you receive, fine-tuning the AI’s outputs to your specific requirements. Incorporate relevant keywords and specific details to guide the AI more effectively towards the desired output.&lt;/p&gt;
&lt;p&gt;Moreover, don’t use jargon or assume knowledge. Be aware of the model’s limitations to craft prompts within its capabilities, avoiding overly complex requests that lead to poor responses. Utilise feedback to continuously improve your prompts, as insights from users or the outputs themselves can guide adjustments for better results.&lt;/p&gt;
&lt;p&gt;Mind the length of your prompts to prevent confusion and higher token consumption, which can increase costs.&lt;/p&gt;
&lt;p&gt;Although the model may be equipped to handle specific challenges like trick questions about prime numbers, this doesn’t guarantee it can manage every query type. Even after conjuring the best prompt, the model can still confidently reply with false information. In some cases, it might only succeed because similar examples were included in its training data, as shown in the examples in the screenshot below:&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/gemini-prime.DO5Cw8VN_ZrB7T8.webp&quot; alt=&quot;Screenshot showing an interaction with an AI model, where the prompt contains trick questions, making it unlikely to provide a factual answer.&quot;&gt;&lt;figcaption&gt;Screenshot showing an interaction with an AI model, where the prompt contains trick questions, making it unlikely to provide a factual answer.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p class=&quot;text-center italic&quot;&gt;
  Screenshot showing an interaction with Gemini, where the prompt contains trick questions, making it unlikely to provide a factual answer.
&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/chatgpt-prime.DhJe535l_Z1ygyWJ.webp&quot; alt=&quot;Screenshot showing an interaction with an AI model, where the prompt contains trick questions, making it unlikely to provide a factual answer.&quot;&gt;&lt;figcaption&gt;Screenshot showing an interaction with an AI model, where the prompt contains trick questions, making it unlikely to provide a factual answer.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p class=&quot;text-center italic&quot;&gt;
  Screenshot of prompting ChatGPT to provide unbiased, factual mathematical answers about the possibility of prime numbers ending in 42.
&lt;/p&gt;
&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing thoughts&lt;a class=&quot;heading-anchor&quot; href=&quot;#closing-thoughts&quot; aria-label=&quot;Permalink to Closing thoughts&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you find yourself facing a complex problem and need assistance in crafting effective prompts, don’t hesitate to ask AI for help. Leveraging the AI’s capabilities can provide valuable insights and suggestions to refine your prompts, ensuring you get the best possible outcomes. If you feel confident in your prompting capabilities, put yourself to the test against &lt;a href=&quot;https://gandalf.lakera.ai/&quot;&gt;Gandalf&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, different types of prompts complement each other, and combining them can lead to more effective and nuanced interactions with AI models. Whether you’re using One-Shot prompts for quick adaptation, Zero-Shot prompts for versatility, or hybrid prompts for complex tasks, understanding how to leverage these techniques together can significantly enhance the AI’s performance and the overall user experience.&lt;/p&gt;
</content:encoded></item><item><title>The Future of AI Computing: WASM and GenAI</title><link>https://afonsojramos.me/blog/wasm-genai/</link><guid isPermaLink="true">https://afonsojramos.me/blog/wasm-genai/</guid><description>My long term bet on Web Assembly (WASM) and Generative AI (GenAI) to power the future of AI computing.</description><pubDate>Mon, 02 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;aside class=&quot;callout callout-note&quot; role=&quot;note&quot;&gt;
&lt;div class=&quot;callout-title&quot;&gt;Also published elsewhere&lt;/div&gt;
&lt;p&gt;This article was also published on the YLD blog &lt;a href=&quot;https://www.yld.io/blog/the-key-to-building-smarter-scalable-ai-powered-applications&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/aside&gt;
&lt;p&gt;While Generative AI (GenAI) has taken centre stage, Web Assembly (WASM) works in the background, complementing GenAI by shifting how we approach performance, portability, and security in web and cross-platform applications.&lt;/p&gt;
&lt;p&gt;While these technologies may seem distinct, their combination unlocks exciting possibilities: WASM’s ability to deliver near-native performance in constrained environments perfectly complements the computation-heavy demands of GenAI models.&lt;/p&gt;
&lt;p&gt;Together, they create a powerful partnership, improving how we deploy and experience AI-driven applications.&lt;/p&gt;
&lt;p&gt;This article explores how GenAI and WASM work together to unlock new possibilities for businesses using AI applications. You will also learn some strategies to harness their full potential with accessibility and scalability in mind.&lt;/p&gt;
&lt;nav class=&quot;table-of-contents&quot;&gt;&lt;ol&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#a-perfect-pair-for-the-future-of-ai-computing&quot; title=&quot;A perfect pair for the future of AI computing&quot;&gt;A perfect pair for the future of AI computing&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#opportunities-challenges-and-key-considerations&quot; title=&quot;Opportunities, challenges, and key considerations&quot;&gt;Opportunities, challenges, and key considerations&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#real-world-use-cases&quot; title=&quot;Real-world use cases&quot;&gt;Real-world use cases&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#overcoming-barriers-to-realise-wasm-and-genais-full-potential&quot; title=&quot;Overcoming barriers to realise WASM and GenAI’s full potential&quot;&gt;Overcoming barriers to realise WASM and GenAI’s full potential&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#how-wasm-and-genai-are-redefining-the-future-of-software-development&quot; title=&quot;How WASM and GenAI are redefining the future of software development&quot;&gt;How WASM and GenAI are redefining the future of software development&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/nav&gt;&lt;h2 id=&quot;a-perfect-pair-for-the-future-of-ai-computing&quot;&gt;A perfect pair for the future of AI computing&lt;a class=&quot;heading-anchor&quot; href=&quot;#a-perfect-pair-for-the-future-of-ai-computing&quot; aria-label=&quot;Permalink to A perfect pair for the future of AI computing&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;WASM is a low-level, portable binary instruction format designed for safe, efficient execution across various environments. Initially created to enhance web application performance, it has quickly extended its reach to edge computing, serverless platforms, and embedded systems. Its appeal lies in three key strengths:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Near-native performance&lt;/li&gt;
&lt;li&gt;Portability as a universal execution target&lt;/li&gt;
&lt;li&gt;Security through its sandboxed environment (especially in browsers)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By decoupling code execution from specific platforms or architectures, WASM allows developers to build once and deploy anywhere, making this function an ideal approach to developing modern, distributed applications.&lt;/p&gt;
&lt;p&gt;The rise of Vercel-like WASM-focused companies highlights the growing adoption of WASM’s unique benefits for developers. With WASM, developers gain greater autonomy and flexibility, enabling them to build and deploy high-performance applications without being tied to specific platforms. Additionally, companies using WASM can optimise performance while minimising resource usage, making the process of building and deploying applications much more efficient.&lt;/p&gt;
&lt;p&gt;Generative AI, on the other hand, has rapidly advanced with models like o1 and Deepseek-R1 now capable of creating human-like text, generating realistic images, and even writing functional code. These models rely on vast amounts of training data and significant computational power, highlighting their potential for major innovation. At the same time, they spark important discussions about the resources required and the ethical considerations surrounding this powerful technology.&lt;/p&gt;
&lt;h2 id=&quot;opportunities-challenges-and-key-considerations&quot;&gt;Opportunities, challenges, and key considerations&lt;a class=&quot;heading-anchor&quot; href=&quot;#opportunities-challenges-and-key-considerations&quot; aria-label=&quot;Permalink to Opportunities, challenges, and key considerations&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;GenAI models are computationally intensive, making them difficult to deploy in resource-constrained environments. Real-time applications, which require fast responses, face challenges with traditional deployment methods, as they struggle to process data quickly enough. This leads to slower performance or, in some cases, failure to deploy. While tools like Ollama have proven that most models can run locally, the performance that you can find on basic devices, such as entry-level smartphones or laptops, is typically much lower than on more powerful systems like the one you’re reading this on.&lt;/p&gt;
&lt;p&gt;However, despite these limitations, GenAI makes an excellent candidate for optimisation through WASM because WASM can make AI applications more accessible across a range of devices. While WASM offers a lightweight and portable execution environment, its ability to address GenAI challenges largely depend on the specific use case and optimisation strategies. Here are some key points to consider:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Portability&lt;/strong&gt;: GenAI models compiled into WASM modules can run across platforms like browsers and edge devices. However, achieving smooth operation may require significant optimisations and adjustments for each environment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Efficiency&lt;/strong&gt;: WASM can improve inference performance in local setups, but the benefits may be limited for larger GenAI models or more complex workloads due to WASM’s current hardware limitations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scalability&lt;/strong&gt;: Serverless platforms adopting WASM can simplify GenAI model deployment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Privacy&lt;/strong&gt;: WASM can help prioritise privacy by enabling GenAI models to run locally, but this approach may involve trade-offs in terms of model complexity and computational overhead.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For example, a WASM module could host a language model directly in a web browser, enabling offline chatbots without needing server connectivity. While tools like Ollama already offer similar functionality, WASM allows for browser-based deployment, which brings the flexibility that comes with the browser.&lt;/p&gt;
&lt;h2 id=&quot;real-world-use-cases&quot;&gt;Real-world use cases&lt;a class=&quot;heading-anchor&quot; href=&quot;#real-world-use-cases&quot; aria-label=&quot;Permalink to Real-world use cases&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In the real world, we’re already seeing examples of WASM and GenAI working together, like &lt;a href=&quot;https://whisper.ggerganov.com/&quot;&gt;Whisper running directly in the browser&lt;/a&gt;. This hybrid approach allows users to choose the model and adjust performance according to their needs. There are exciting possibilities on the horizon where WASM and GenAI could combine to create innovative solutions. Here are a few examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;In-Browser AI Assistants&lt;/strong&gt;: Deploy lightweight GenAI models directly in browsers using WASM, providing real-time assistance without network latency.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge Device Applications&lt;/strong&gt;: Run WASM-optimised GenAI models on IoT devices for tasks like image recognition or anomaly detection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Serverless AI APIs&lt;/strong&gt;: Host GenAI models as WASM modules on serverless platforms, reducing operational costs while improving scalability.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These use cases demonstrate how WASM empowers GenAI to operate efficiently in diverse environments, from cloud servers to smaller edge devices.&lt;/p&gt;
&lt;h2 id=&quot;overcoming-barriers-to-realise-wasm-and-genais-full-potential&quot;&gt;Overcoming barriers to realise WASM and GenAI’s full potential&lt;a class=&quot;heading-anchor&quot; href=&quot;#overcoming-barriers-to-realise-wasm-and-genais-full-potential&quot; aria-label=&quot;Permalink to Overcoming barriers to realise WASM and GenAI’s full potential&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;While the combination of WASM and GenAI promises a lot of great things, several challenges must be addressed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GPU Limitations&lt;/strong&gt;: WASM currently lacks native GPU support, making it difficult to accelerate GenAI workloads that rely on parallel processing, a critical feature for handling large-scale AI tasks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory Constraints&lt;/strong&gt;: This compute boundary is a significant hurdle for WASM when working with AI workloads requiring high levels of parallelism. Furthermore, large GenAI models often exceed WASM’s default memory limits, necessitating careful optimisation to fit these constraints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Early-stage Ecosystem&lt;/strong&gt;: While WASM is evolving rapidly, tools and libraries for integrating WASM with AI frameworks are still maturing, highlighting the ecosystem’s early-stage nature.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Emerging tools like WebLLM and platforms such as Turso are addressing some of these limitations in innovative ways. For example, WebLLM demonstrates the feasibility of running large language models directly in the browser by leveraging WebGPU for acceleration. It’s important to note that WebGPU is still an experimental API, and its integration with WASM for AI workloads is in its early stages, requiring further development and testing.&lt;/p&gt;
&lt;p&gt;Similarly, Turso, a distributed database built on libSQL, has introduced features like native vector search and 1-bit quantisation for vector embeddings. These advancements make it easier to deploy AI applications that prioritise local-first processing and efficient resource usage. However, both WebLLM and Turso highlight the gaps in WASM’s ecosystem, such as the need for GPU integration and optimised toolchains for AI workloads. Addressing these challenges will be key to unlocking the full potential of this pairing.&lt;/p&gt;
&lt;h2 id=&quot;how-wasm-and-genai-are-redefining-the-future-of-software-development&quot;&gt;How WASM and GenAI are redefining the future of software development&lt;a class=&quot;heading-anchor&quot; href=&quot;#how-wasm-and-genai-are-redefining-the-future-of-software-development&quot; aria-label=&quot;Permalink to How WASM and GenAI are redefining the future of software development&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Looking ahead, advancements in both WASM and GenAI promise to deepen their integration through the following ways:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;WebGPU Support&lt;/strong&gt;: Introducing WebGPU to WASM environments will enable hardware acceleration for AI workloads. However, it’s worth noting that &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API&quot;&gt;WebGPU is still an experimental API&lt;/a&gt;, and its integration with WASM for AI workloads is in the early stages, requiring further development and testing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Model Optimisation&lt;/strong&gt;: Techniques like quantisation and pruning will make it easier to deploy GenAI models within WASM’s constraints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Standardised Toolchains&lt;/strong&gt;: Improved tooling will simplify the process of compiling and deploying GenAI models as WASM modules.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Private Personal Assistants&lt;/strong&gt;: Having the model run locally will open up the possibilities to protect the end user from endless data-sharing, one that is a given if the benefits of using AI keep increasing.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These developments are set to enable real-time, AI-driven applications that are fast, portable, and accessible to a wide range of users.&lt;/p&gt;
&lt;p&gt;WASM and GenAI are two disruptive technologies poised to change how we build and deploy software. WASM’s portability and performance make it an ideal runtime for GenAI, enabling the creation of applications that are both powerful and accessible.&lt;/p&gt;
&lt;p&gt;As these technologies continue to evolve, now is the time for developers to explore their synergy. Whether you’re building the next-gen AI assistant, a cutting-edge edge computing solution, or serverless applications, the pairing of WASM and GenAI offers endless possibilities to be unlocked.&lt;/p&gt;
</content:encoded></item><item><title>Spicetify - An open-source journey</title><link>https://afonsojramos.me/blog/spicetify-open-source-journey/</link><guid isPermaLink="true">https://afonsojramos.me/blog/spicetify-open-source-journey/</guid><description>How did I get to be one of the core maintainers of Spicetify? Let&apos;s find out!</description><pubDate>Mon, 12 Dec 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;According to my message history, it all started around February 2020 when I found out about Spicetify through some random Reddit post in the illustrious &lt;a href=&quot;https://www.reddit.com/r/unixporn/&quot;&gt;r/unixporn&lt;/a&gt; - a subreddit where the highest tier of nerds share their Unix setups and configs - and I was immediately intrigued.&lt;/p&gt;
&lt;p&gt;I had been using Spotify for a while, but even though the UI is not &lt;strong&gt;awful&lt;/strong&gt;, they do have some very anti-user behaviours. Initially, I was simply a user, but then ideas to improve it started coming up, and I started to develop a more active role in the development. One of my first contributions was to the &lt;a href=&quot;https://spicetify.app/docs/advanced-usage/custom-apps#new-releases&quot;&gt;New Releases Custom App&lt;/a&gt;. This extension creates a page where we can see all the new releases for the artists we follow. Conceptually, it is a simple feature, however, Spotify’s “notifications” were practically useless, even to this day&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;, which made it quickly become one of my favourite Spicetify features, which also led to my first contribution (&lt;a href=&quot;https://github.com/spicetify/spicetify-cli/issues/247&quot;&gt;spicetify-cli#247&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Spicetify’s core, what we call &lt;a href=&quot;https://github.com/spicetify/spicetify-cli&quot;&gt;spicetify-cli&lt;/a&gt;, is a Command-line Interface (CLI) tool that goes through Spotify’s binaries mutating them through regex matching to allow for the promised customisation. It is written in Go, which is a highly performant language that I learnt a couple of years prior to give a &lt;a href=&quot;https://github.com/afonsojramos/competitive-programming/blob/master/advent-of-code/2018/go-guide.md&quot;&gt;workshop&lt;/a&gt; on it at &lt;a href=&quot;https://ieee.fe.up.pt/&quot;&gt;IEEE University of Porto Student Branch&lt;/a&gt;, a student branch of the IEEE that I was a member of at the time - and later was elected Vice-President for a year.&lt;/p&gt;
&lt;p&gt;With time, I started helping out more and more in handling issues, developing small features, and keeping the latest Spotify version supported&lt;sup&gt;&lt;a href=&quot;#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;. Eventually, &lt;a href=&quot;https://github.com/khanhas&quot;&gt;khanhas&lt;/a&gt; made me a maintainer on GitHub to help with everything. In late 2021, Spicetify’s popularity truly blew up and we had to start handling a lot more issues and pull requests, which was a very exciting time for me. However, it was also the last time we saw khanhas, as he decided to leave the project and focus on other things. Thankfully, he left the project in a very good state, and I was able to take over as the new maintainer. Additionally, there were a small group of people that were already helping out with the project, which helped immensely.&lt;/p&gt;
&lt;p&gt;It was September 2020 when I started working full-time, but it was in 2022 that my workload, other side-projects and overall life started pilling up a bit more, and, eventually, I noticed myself not being able to dedicate as much time to Spicetify as I would have liked. This is when I shifted to a more managerial role, where I would help out with the more complex issues, but also help out with the more mundane tasks, such as reviewing pull requests and merging them. I also created the &lt;a href=&quot;https://spicetify.app&quot;&gt;Spicetify Documentation&lt;/a&gt; website, which is a very important part of the project, as it is the first place people look at when they want to customise their Spotify, unlike previously when people could only look at GitHub’s wiki, which was not very user-friendly for newcomers. And also created the &lt;a href=&quot;https://github.com/spicetify&quot;&gt;Spicetify GitHub organisation&lt;/a&gt;, which is where all our repositories are hosted since previously they were hosted across several different contributors’ accounts.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://afonsojramos.me/_astro/spicetify-visitors.TetB9ZvF_Z15fWq2.webp&quot; alt=&quot;Spicetify Docs Visitors&quot;&gt;&lt;figcaption&gt;Spicetify Docs Visitors&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Overall, it has been quite a ride, but seeing hundreds of thousands of downloads per GitHub release and over 200K unique visitors per month to our documentation is a very rewarding feeling. I am very grateful to all the contributors, users and maintainers that have helped out in this journey, and I hope that we can continue to make Spicetify better and better.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;nav class=&quot;table-of-contents&quot;&gt;&lt;ol&gt;&lt;li class=&quot;table-of-contents-item table-of-contents-depth-2&quot;&gt;&lt;a href=&quot;#footnote-label&quot; title=&quot;Footnotes&quot;&gt;Footnotes&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/nav&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;a class=&quot;heading-anchor&quot; href=&quot;#footnote-label&quot; aria-label=&quot;Permalink to Footnotes&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;They did do &lt;strong&gt;something&lt;/strong&gt; on mobile, but if you follow more than 30 artists the list gets pretty polluted with singles. The only filter available is either Music or Podcasts, which is not very useful. And I mostly listen to Spotify on my PC anyway, so it doesn’t really matter. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Spotify releases most of the times do not break the CLI itself, but when they do it is because we need to apply updates to the regexes that match the Spotify binaries. This is a very tedious process, but it is crucial to keep the CLI working. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded></item><item><title>Welcome to my blog</title><link>https://afonsojramos.me/blog/opening-remarks/</link><guid isPermaLink="true">https://afonsojramos.me/blog/opening-remarks/</guid><description>The first post is always the worst</description><pubDate>Mon, 14 Jun 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;println!(&quot;Hello World!&quot;);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;As a learning &lt;strong&gt;rust&lt;/strong&gt;acean, I’ve chosen to start this blog with a classic, but &lt;strong&gt;rust&lt;/strong&gt;ic &lt;em&gt;(has this joke been made before?)&lt;/em&gt;, Hello World!&lt;/p&gt;
&lt;p&gt;Bad programming jokes aside, I will use this blog to write posts about tech, music, states of mind, opinions, etc. Maybe I’ll even do one about beer 🍺! In reality, I don’t quite have any idea what I will write about, maybe about everything and maybe about nothing. But I’ll certainly try to write about something, even if it is an excuse to fuel my creative side!&lt;/p&gt;
&lt;p&gt;As an engineer, I often consider myself too analytic in the creative process. Not that it is a bad thing, it’s just something that is part of me and that I am aware of. It is not for no reason that I’ve decided to focus on backend development, as problem-solving is my jam! However, if it involves designing, I’ll probably rely a lot on the “inspiration” phase, which roughly translates to &lt;em&gt;“look at other designs and rip off all the good ideas”&lt;/em&gt;. Again, this is also not a bad thing - &lt;em&gt;I think&lt;/em&gt; - but it means that my creative work will never be truly &lt;em&gt;original&lt;/em&gt; and be based on building on what’s good ✨ out there ✨.&lt;/p&gt;
&lt;p&gt;In the end, my admiration for the creative process of artists, designers and writers is immeasurable. It’s something that can be easily noticed by looking at the amount of music I listen to and concerts that I go to, but it goes way beyond that! We all have some sort of creativity within us, but some people are so much better at using it and that makes them amazing. And I hope that by creating this blog I can practice that, expand my creative side and that you enjoy this journey I am embarking on.&lt;/p&gt;
&lt;p&gt;See you in the next post! ✌&lt;/p&gt;
</content:encoded></item></channel></rss>