<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Philippe Serhal — Software Engineer</title><description>Philippe Serhal is an experienced Software Engineer. He makes computers solve problems.</description><link>https://philippeserhal.com/</link><language>en-us</language><item><title>OSS made me a better developer</title><link>https://philippeserhal.com/articles/oss-made-me-a-better-developer/</link><guid isPermaLink="true">https://philippeserhal.com/articles/oss-made-me-a-better-developer/</guid><description>How getting involved in npmx made me a better developer</description><pubDate>Tue, 03 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I recently got involved in an open-source project, &lt;a href=&quot;https://npmx.dev&quot;&gt;npmx&lt;/a&gt;. This is the meandering story of how that made me a better developer.&lt;/p&gt;
&lt;h2 id=&quot;its-never-too-late-to-start-writing-accessible-code&quot;&gt;It’s never too late to start writing accessible code&lt;/h2&gt;
&lt;p&gt;I started my career in 2007 at a Canadian government department responsible for web standards. As you can imagine, accessibility was a pillar. We tested websites (actually, there was a standard that mandated spelling it “Web site”; it still haunts me) on a dedicated computer in a closet that had &lt;a href=&quot;https://en.wikipedia.org/wiki/JAWS_(screen_reader)&quot;&gt;expensive assistive screen reading software&lt;/a&gt; installed, and we reported back to departments that didn’t meet the bar. That bar was not very high back then. There was no WCAG 2.0 and no ARIA yet. &lt;a href=&quot;https://web.archive.org/web/20150910072359/http://adaptivepath.org/ideas/ajax-new-approach-web-applications/&quot;&gt;Gmail and Google Maps had just shown what was possible&lt;/a&gt; and the web had become a dynamic free-for-all with little concern for accessibility.&lt;/p&gt;
&lt;p&gt;Over the next couple decades, I did graduate research in computational biology, built internal apps for e-commerce fulfillment and delivery operations at a startup, led an internal developer productivity team, and maintained the build platform at Netlify, including all its framework integrations. All this time I wasn’t closely touching production web development for large-scale, diverse audiences. Meanwhile, the capabilities of the web exploded and accessibility patterns evolved with it. I didn’t go out of my way to keep up, neither professionally nor in my countless side projects.&lt;/p&gt;
&lt;p&gt;Then in early 2026 I joined a group of people building a modern web app for millions of users. They took accessibility as a given and simply put in the work. PRs weren’t merged if they didn’t meet the accessibility bar. I had to learn if I was going to contribute. I mean, I had to know what I was doing if I was going to be a &lt;em&gt;maintainer&lt;/em&gt;, right?!&lt;/p&gt;
&lt;p&gt;So I just… learned. I browsed the codebase, learned the patterns, looked up attributes I wasn’t familiar with, got feedback from the linter, from the automated Lighthouse audit and component accessibility tests. I asked Claude for an accessibility review before putting up each PR. I learned from PR review feedback, on my own PRs and on others’. I learned from our &lt;code&gt;#a11y&lt;/code&gt; Discord channel.&lt;/p&gt;
&lt;p&gt;One month later, I’m far from an expert, but I’ve shifted a lot out of my “things I don’t know I don’t know” bucket; more than ever, with the ease of retrieving information, knowing &lt;em&gt;what&lt;/em&gt; to retrieve is what matters most.&lt;/p&gt;
&lt;p&gt;Now I’m going back to all my live side projects to set up a11y tooling and make them accessible to a more diverse audience. Because it’s a given for &lt;em&gt;me&lt;/em&gt; now too. It isn’t too late &lt;a href=&quot;https://www.w3.org/WAI/fundamentals/accessibility-intro/&quot;&gt;for you either&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;internationalization-is-easy-with-a-community&quot;&gt;Internationalization is easy… with a community&lt;/h2&gt;
&lt;p&gt;My internationalization journey follows a similar arc as above. At my Government of Canada job, all websites were required to be in English and French, the two official languages of Canada. The ColdFusion(!) server read from a Microsoft SQL Server 2005(!) table containing translated strings. Translations were updated by &lt;em&gt;manually writing and executing raw SQL queries ad hoc against the production database&lt;/em&gt;(!). Nominally, the process had to go through a lengthy internal request process from some government translation department, even for a single word. In practice, as I was the only French speaker on the team and a junior programmer… it fell on me. (One time, I hit enter on my &lt;code&gt;UPDATE strings SET en = &amp;quot;something&amp;quot;, fr = &amp;quot;quelque chose&amp;quot;;&lt;/code&gt; query before remembering to add the &lt;code&gt;WHERE key = &amp;quot;something&amp;quot;&lt;/code&gt; clause… I’ll never forget the monstrosity of a website that created.)&lt;/p&gt;
&lt;p&gt;Over the next 19 years, I never really encountered internationalization again. (Well, okay, as part of my job on the frameworks team at Netlify I do occasionally tear my hair out at the complexity wrought by Next.js’s built-in i18n features.)&lt;/p&gt;
&lt;p&gt;It turns out internationalization hasn’t changed all that much in two decades. You extract all user-facing strings somewhere, each mapped to a unique key with translations. You avoid making assumptions about the length of those strings or their direction… The hard part is staying organized, keeping track of what needs to be translated or re-translated, who can translate and review what, which strings are unused and can be cleaned up, and so on. Some of this can be solved with tooling and automation, while some of it is just humans working together. &lt;a href=&quot;https://lunaria.dev/&quot;&gt;Lunaria&lt;/a&gt; solved some of these problems for npmx.&lt;/p&gt;
&lt;p&gt;One month in, we support &lt;strong&gt;28 locales&lt;/strong&gt;. It sort of just… happened, organically.&lt;/p&gt;
&lt;p&gt;As with accessibility, I now know that I know or know that I don’t know a number of things that I previously didn’t know that I didn’t know about internationalization and localization. Nope, there was definitely no easier way to put that, I’m sorry.&lt;/p&gt;
&lt;h2 id=&quot;nuxt-really-shines-with-many-contributors&quot;&gt;Nuxt really shines with many contributors&lt;/h2&gt;
&lt;p&gt;I wasn’t new to &lt;a href=&quot;https://nuxt.com&quot;&gt;Nuxt&lt;/a&gt;. It’s my job at Netlify to know a dozen frameworks well. I built &lt;a href=&quot;https://npmx.dev/package/@netlify/nuxt&quot;&gt;&lt;code&gt;@netlify/nuxt&lt;/code&gt;&lt;/a&gt; (with Daniel Roe!) and help maintain the Netlify Nitro presets. But building one-off demo apps, test fixtures, and small personal projects does not quite expose you to the same strengths and weaknesses of a framework as does collaborating with 200 contributors over just a few weeks.&lt;/p&gt;
&lt;p&gt;I knew this in theory, but seeing in practice how Nuxt’s &lt;em&gt;highly&lt;/em&gt; opinionated, batteries-included conventions nudge all contributors toward a common &lt;a href=&quot;https://learn.microsoft.com/en-us/archive/blogs/brada/the-pit-of-success&quot;&gt;Pit of Success&lt;/a&gt; has been eye opening. It isn’t a panacea, but much like automatic code formatting it avoids entire classes of &lt;a href=&quot;https://bikeshed.com/&quot;&gt;bikeshedding&lt;/a&gt; and lets everyone focus on shipping features. Two hundred new contributors, most of whom had never worked in Nuxt, were able to effectively contribute over a thousand PRs in just a few weeks. That’s saying something.&lt;/p&gt;
&lt;h2 id=&quot;i-was-missing-out-on-great-tools&quot;&gt;I was missing out on great tools&lt;/h2&gt;
&lt;p&gt;There’s something about OSS where they are always way ahead of the curve in terms of their own developer tools (don’t be shy, I know you’re still using jest and webpack at work). A greenfield OSS project that is &lt;em&gt;itself&lt;/em&gt; a developer tool, built by some of the brightest minds in the developer tooling space, is bound to be a goldmine. Here are just a few of those that stood out to me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://knip.dev&quot;&gt;knip&lt;/a&gt;: knip is incredible. If like me you’d used &lt;code&gt;npx knip&lt;/code&gt; here and there for a one-off report, trust me, give it a fresh try—it’s become &lt;em&gt;really&lt;/em&gt; good, has really cut down on false positives, and is highly configurable. Install it as a dev dependency in your projects, clean up the clutter, and run it automatically in CI from now on. Oh, and it now has IDE integrations, an LSP server, and an MCP server.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://npmx.dev/package/@e18e/eslint-plugin&quot;&gt;e18e eslint plugin&lt;/a&gt;: The &lt;a href=&quot;https://e18e.dev/&quot;&gt;e18e&lt;/a&gt; crew are now in your linter. You can have your own little James Garbutt in your IDE calling out performance gotchas and bloated dependencies. Don’t worry, eslint plugins now work with…&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oxc.rs/docs/guide/usage/linter.html&quot;&gt;oxlint&lt;/a&gt;: Extremely fast, eslint-compatible linter. It’s here. There’s no need to wait any longer.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://oxc.rs/docs/guide/usage/formatter.html&quot;&gt;oxfmt&lt;/a&gt;: Extremely fast, prettier-compatible formatter. It’s now in beta and it works great. Hey, it’s a &lt;em&gt;formatter&lt;/em&gt;, you can afford to use a beta.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vue-data-ui.graphieros.com/&quot;&gt;vue-data-ui&lt;/a&gt;: Powerful, beautiful, &lt;a href=&quot;https://vue-data-ui.graphieros.com/customization&quot;&gt;absurdly full featured and customizable&lt;/a&gt; data visualization Vue library.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://unocss.dev/&quot;&gt;unocss&lt;/a&gt;: Lightweight, portable highly customizable atomic utility CSS engine. Tailwind compatible.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’ll be adding these and more to my toolbox.&lt;/p&gt;
&lt;h2 id=&quot;you-can-just-build-what-you-wish-existed&quot;&gt;You can just build what you wish existed&lt;/h2&gt;
&lt;p&gt;Now more than ever, if there is something missing that software can solve for you, for your family, for your friends, for your employer, for your client, you (&lt;a href=&quot;https://www.netlify.com/blog/you-are-now-a-developer/&quot;&gt;yes, you&lt;/a&gt;) can just build it. If existing software isn’t cutting it, you can just build your own. If existing software is funded by interests whose actions are fundamentally incompatible with your values, you can just build your own.&lt;/p&gt;
&lt;p&gt;In all these cases, you can go much, much farther by finding like-minded people and building together.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hold fast to the one noble thing.&lt;/p&gt;
&lt;p&gt;— &lt;em&gt;Four Ways to Forgiveness&lt;/em&gt;, Ursula K. Le Guinn (1995)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;you-can-find-your-people-again-and-again&quot;&gt;You can find your people again and again&lt;/h2&gt;
&lt;p&gt;I’ve heard that a key to happiness and fulfillment is to &lt;em&gt;find your people&lt;/em&gt;. I think that’s incomplete: you have to &lt;em&gt;keep finding more people&lt;/em&gt;; all sorts of people, across all the spheres of your multifaceted existence. Find the people who share your sense of humour. Find the people who share that weird, technical passion of yours, and those obsessed with that obscure fantasy author you love. Find the people who share your fundamental values (P.S.: really hold on to these most of all). It turns out there’s quite a large and varied supply of &lt;em&gt;people&lt;/em&gt; out there, and you never know what might make them &lt;em&gt;yours&lt;/em&gt; after all.&lt;/p&gt;
&lt;p&gt;In npmx, I found people who &lt;em&gt;cared&lt;/em&gt; about a daily challenge they and others were facing, and &lt;em&gt;believed&lt;/em&gt; they could come together and… just build the solution—without putting others down, without monetization, with inclusivity in mind from the start, just building, building, building without borders. Devs already working on a solution to this? Join our team! You, you and you! Have ideas from another problem space that connect these two worlds? Oh hello, atproto friends, come on in! Leading expert on a11y passionate about making this a rich experience for all abilities? Yes please, teach us your ways.&lt;/p&gt;
&lt;p&gt;These are my people. They may be your people.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Then you came here,” she said.&lt;/p&gt;
&lt;p&gt;“Then I came here.”&lt;/p&gt;
&lt;p&gt;“And learned?”&lt;/p&gt;
&lt;p&gt;“How to walk,” he said. “How to walk with my people.”&lt;/p&gt;
&lt;p&gt;— &lt;em&gt;A Man of the People (Four Ways to Forgiveness)&lt;/em&gt;, Ursula K. Le Guinn (1995)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;a-few-of-my-people&quot;&gt;A few of my people&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://npmx.dev/blog/alpha-release&quot;&gt;&lt;strong&gt;Announcing npmx: a fast, modern browser for the npm registry&lt;/strong&gt;&lt;/a&gt; — npmx.dev&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://whitep4nth3r.com/blog/how-to-make-your-first-open-source-contribution/&quot;&gt;&lt;strong&gt;How to Make Your First Open Source Contribution&lt;/strong&gt;&lt;/a&gt; — whitep4nth3r.com — Getting involved in open source doesn’t have to be scary! Understand how to find a great project and make your first contribution in this guide from Salma.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://roe.dev/blog/a-virtuous-cycle&quot;&gt;&lt;strong&gt;A Virtuous Circle&lt;/strong&gt;&lt;/a&gt; — danielroe.dev — There’s a reason why building npmx has been such a blast so far, and it’s one of the most powerful patterns in open source software development. It’s also why ‘the 10x developer’ is an incredibly dangerous myth.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://patak.cat/npmx/converging-communities&quot;&gt;&lt;strong&gt;npmx: converging communities&lt;/strong&gt;&lt;/a&gt; — The story of the many people and communities that converged to build npmx together.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.netlify.com/blog/sponsoring-npmx&quot;&gt;&lt;strong&gt;Sponsoring npmx&lt;/strong&gt;&lt;/a&gt; — It’s more important than ever that companies come together across competitive boundaries to sponsor and support the open ecosystem that lifts all boats.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://graphieros.github.io/graphieros-blog/blog/2026/npmx.html&quot;&gt;&lt;strong&gt;vue-data-ui is on npmx npmx is on vue-data-ui&lt;/strong&gt;&lt;/a&gt; — graphieros.com — Graphieros explores a minimal npm-based workflow and why it exists.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.alexdln.com/blog/npmx-the-month&quot;&gt;&lt;strong&gt;The month. npmx&lt;/strong&gt;&lt;/a&gt; — alexdln.com — Alex reflects on the project, warm stories, wonderful people, and a look into the future.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://e18e.dev/blog/npmx-collaboration.html&quot;&gt;&lt;strong&gt;Collaborating with npmx&lt;/strong&gt;&lt;/a&gt; — How the e18e community is collaborating closely with npmx to make best practices more visible and accessible to everyone in the ecosystem.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://piccalil.li/blog/finding-an-accessibility-first-culture-in-npmx/&quot;&gt;&lt;strong&gt;Finding an accessibility-first culture in npmx&lt;/strong&gt;&lt;/a&gt; — Abbey Perini talks about how accessibility is a deep part of the npmx culture.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://johnnyreilly.com/npmx-with-a-little-help-from-my-friends&quot;&gt;&lt;strong&gt;npmx: With a Little Help From My Friends&lt;/strong&gt;&lt;/a&gt; — johnnyreilly.com — How to contribute to npmx.dev, and thoughts on Johnny’s experience with the project.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://opensourcepledge.com/blog/npmx-a-lesson-in-open-source-collaboration-feedback-loops/&quot;&gt;&lt;strong&gt;npmx: A Lesson in Open Source’s Collaboration Feedback Loops&lt;/strong&gt;&lt;/a&gt; — opensourcepledge.com — npmx’s success is reminding us why Open Source is such a special social phenomenon.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.trueberryless.org/blog/npmx/&quot;&gt;&lt;strong&gt;Rising community at tomorrow’s horizon&lt;/strong&gt;&lt;/a&gt; — trueberryless.org — Telling the story of a newly founded community.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://conf.zurichjs.com/blog/open-source-needs-community&quot;&gt;&lt;strong&gt;Open source needs community. Community needs open source.&lt;/strong&gt;&lt;/a&gt; — zurichjs.com — Why ZurichJS cares about getting people into open source.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.sybers.fr/blog/3mfhn5xoawz24&quot;&gt;&lt;strong&gt;From a Bluesky post to my favorite open source community&lt;/strong&gt;&lt;/a&gt; — sybers.fr — The best open source projects aren’t just about great code. They’re about the people behind them.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://storybook.js.org/blog/storybook-npmx&quot;&gt;&lt;strong&gt;Storybook 💙 npmx&lt;/strong&gt;&lt;/a&gt; — storybook.js.org — We’re huge fans of what the npmx community is building. Today’s alpha is just the starting line, and we’re proud to be running alongside them.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jensroemer.com/writing/open-source-whats-in-it-for-me/&quot;&gt;&lt;strong&gt;Open source, what’s in it for me?&lt;/strong&gt;&lt;/a&gt; — jensroemer.com — Reflections on learning, community, and change.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://news.atmosphereconf.org/3mg5b3zvktc2i&quot;&gt;&lt;strong&gt;npmx goes social with atproto&lt;/strong&gt;&lt;/a&gt; — Announcing npmx speakers, and congratulations on launch day!&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://voidzero.dev/posts/npmx-alpha&quot;&gt;&lt;strong&gt;VoidZero and npmx: Building Better Tools Together&lt;/strong&gt;&lt;/a&gt; — voidzero.dev — How VoidZero and npmx.dev share a vision for making JavaScript developers more productive, and how real-world feedback from open-source builders helps improve our tooling.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://youtu.be/NoC5U6F6p4Y&quot;&gt;&lt;strong&gt;The npmjs.com that developers deserve - What is npmx? (video)&lt;/strong&gt;&lt;/a&gt; — An introductory video showcasing Alex’s favorite features of npmx and the open-source idea behind it.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.faziz-dev.com/blog/community-open-source-and-npmx&quot;&gt;&lt;strong&gt;Community, Open Source, and npmx&lt;/strong&gt;&lt;/a&gt; — farisaziz12.bsky.social — npmx isn’t just an npm browser, it’s a fast-moving open source train that welcomes you aboard the moment you show up.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://paulie.codes/blog/3mfs2stugzp2v&quot;&gt;&lt;strong&gt;Overcoming Imposter Syndrome: My First Open Source Contribution&lt;/strong&gt;&lt;/a&gt; — paulie.codes — The most important part of open source is the people, and everyone has something valuable to bring to the table.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.radosvet.dev/posts/career/from-newsletter-to-open-source&quot;&gt;&lt;strong&gt;From a Newsletter Link to My First Open Source Contribution&lt;/strong&gt;&lt;/a&gt; — How discovering npmx through a newsletter led to a first meaningful open source contribution and a new perspective on community-driven development.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vale.rocks/micros/20260303-1200&quot;&gt;&lt;strong&gt;npmx Is Open-Source Done Right&lt;/strong&gt;&lt;/a&gt; — How the ethos and practices of npmx represent a healthy open-source ecosystem that should be the standard, not an exception.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jaydip.me/blog/joy-of-open-source&quot;&gt;&lt;strong&gt;Joy of open source&lt;/strong&gt;&lt;/a&gt; — childish fun of making things together.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Web frameworks: 2025 in review</title><link>https://philippeserhal.com/articles/web-frameworks-2025-year-in-review/</link><guid isPermaLink="true">https://philippeserhal.com/articles/web-frameworks-2025-year-in-review/</guid><description>13 major releases, 52 security vulnerabilities, and 3 acquisitions shaped the web framework ecosystem in 2025. A comprehensive look at what happened and what is coming in 2026.</description><pubDate>Tue, 27 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;2025 brought 13 major framework releases and 52 security vulnerabilities across the most widely used fullstack frameworks. Many weren’t obscure edge cases, but issues that forced maintainers, platforms, and developers to confront the real cost of abstraction.&lt;/p&gt;
&lt;p&gt;As a software engineer on the frameworks team at Netlify, my job is to track changes like these closely, support new releases on day one, and make sure they don’t become your problem.&lt;/p&gt;
&lt;p&gt;This post covers what happened in 2025. I’ll walk through six themes that defined the year, with context on how we got here and what they signal for teams building and shipping in 2026.&lt;/p&gt;
&lt;p&gt;What we’ll cover:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Security under the microscope&lt;/li&gt;
&lt;li&gt;The Next.js team delivered&lt;/li&gt;
&lt;li&gt;Agent experience became a thing&lt;/li&gt;
&lt;li&gt;React Server Components matured&lt;/li&gt;
&lt;li&gt;Signals won – except in React&lt;/li&gt;
&lt;li&gt;Performance ‘Rustification’ continued&lt;/li&gt;
&lt;/ol&gt;
&lt;figure&gt;&lt;img src=&quot;https://philippeserhal.com/_astro/framework-updates-2025.i-fYpFPC.png&quot; alt=&quot;Framework updates in 2025&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;security-under-the-microscope&quot;&gt;Security under the microscope&lt;/h2&gt;
&lt;p&gt;This year saw vastly increased scrutiny from security researchers. No fullstack web framework was spared.&lt;/p&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Framework&lt;/th&gt;&lt;th&gt;Vulnerabilities&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Nuxt&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;1 high-severity CVE (&lt;a href=&quot;https://github.com/nuxt/nuxt/security/advisories/GHSA-jvhm-gjrh-3h93&quot;&gt;7.5&lt;/a&gt;) leading to CDN cache poisoning&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Remix / React Router 7&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;3 high-severity CVEs (&lt;a href=&quot;https://www.netlify.com/changelog/security-update-react-router-remix-vulnerabilities/&quot;&gt;7.5, 7.5, 8.2&lt;/a&gt;), plus &lt;a href=&quot;https://github.com/remix-run/react-router/security/&quot;&gt;6 more&lt;/a&gt; before publication&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Next.js&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;4 low, 5 moderate, 3 high, 2 critical—including a &lt;a href=&quot;https://www.netlify.com/changelog/2025-12-03-react-security-vulnerability-response/&quot;&gt;10.0 that rippled across the React ecosystem&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Astro&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://github.com/withastro/astro/security&quot;&gt;2 low, 7 medium, 5 high&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Angular&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;3 high-severity CVEs (&lt;a href=&quot;https://github.com/angular/angular/security/&quot;&gt;7.1, 7.7, 8.5&lt;/a&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;SvelteKit&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://www.netlify.com/changelog/2026-01-15-sveltekit-security-vulnerabilities/&quot;&gt;4 high, 1 moderate&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The deep dive on the Remix/React Router vulnerability by &lt;a href=&quot;https://zhero-web-sec.github.io/research-and-things/react-router-and-the-remixed-path&quot;&gt;Rachid Allam&lt;/a&gt; is worth nerding out on.&lt;/p&gt;
&lt;p&gt;With most of these 52 vulnerabilities involving servers, some developers quietly held their “I told you so”s while yearning for a time when frontend frameworks didn’t touch the backend at all.&lt;/p&gt;
&lt;h2 id=&quot;the-nextjs-team-delivered&quot;&gt;The Next.js team delivered&lt;/h2&gt;
&lt;p&gt;After years of criticism around transparency, vendor lock-in, and ecosystem fragmentation, 2025 was the year the Next.js team took &lt;a href=&quot;https://www.netlify.com/blog/how-we-run-nextjs/&quot;&gt;constructive feedback&lt;/a&gt; to heart.&lt;/p&gt;
&lt;p&gt;The team proposed a &lt;a href=&quot;https://github.com/vercel/next.js/discussions/77740&quot;&gt;Deployment Adapter API&lt;/a&gt; and shipped an alpha in Next.js 16—designed in close collaboration with engineers at Netlify, Cloudflare, Google Firebase App Hosting, Deno, SST, and AWS Amplify.&lt;/p&gt;
&lt;p&gt;This working group opened lines of communication across the ecosystem: they shared early RFC drafts, gave advance notice of upcoming releases, and coordinated planning. A security group was also established where Next.js platform providers receive early notice (under embargo) of CVEs, giving time to apply platform mitigations, patch adapters, and prepare customer communications.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://philippeserhal.com/_astro/nextjs-deployment-adapter-api.DN25ZzzG.png&quot; alt=&quot;A slide from Next.js Conf 2025 - Next.js Deployment Adapters API: A clear contract for a predictable platform integration&quot;&gt;&lt;figcaption&gt;&lt;p&gt;Source: &lt;a href=&quot;https://www.youtube.com/watch?v=myjrQS_7zNk&amp;#38;t=2151s&quot;&gt;Next.js Conf
2025&lt;/a&gt;&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;These are major positive developments for Next.js developers and fans of the open web. Credit goes to &lt;a href=&quot;https://jimmyl.ai/&quot;&gt;Jimmy Lai&lt;/a&gt; and the whole Next.js team for turning over a new leaf.&lt;/p&gt;
&lt;h2 id=&quot;agent-experience-became-a-thing&quot;&gt;Agent experience became a thing&lt;/h2&gt;
&lt;p&gt;As AI coding agents moved from weekend vibe coding to daily workflow, frameworks started explicitly shipping agent experience (AX) improvements.&lt;/p&gt;
&lt;p&gt;Out of the box, browser logs weren’t visible to agents. Next.js shipped a dead-simple solution: forward these to the dev server logs. Other frameworks &lt;a href=&quot;https://github.com/vitejs/vite/pull/20916&quot;&gt;will follow&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://philippeserhal.com/_astro/tweet-rob-pruzan.BZrEd_3u.png&quot; alt=&quot;Tweet from Rob Pruzan: soon in @nextjs, browser errors &amp;#38; logs can show in your terminal for your AI to read&quot;&gt;&lt;figcaption&gt;&lt;p&gt;Source: &lt;a href=&quot;https://twitter.com/RobKnight__/status/1937724464395092137&quot;&gt;@RobKnight__ on
X&lt;/a&gt;&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Every major framework shipped its own MCP server in 2025:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://angular.dev/ai/mcp&quot;&gt;Angular&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/build-with-ai/#astro-docs-mcp-server&quot;&gt;Astro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/docs/app/guides/mcp&quot;&gt;Next.js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nuxt.com/blog/building-nuxt-mcp&quot;&gt;Nuxt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://svelte.dev/docs/mcp/overview&quot;&gt;Svelte&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tanstack.com/mcp/latest&quot;&gt;TanStack Start (alpha, just before publication)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In October, Ryan Florence and Michael Jackson unveiled their vision for &lt;a href=&quot;https://remix.run/blog/remix-jam-2025-recap&quot;&gt;Remix 3&lt;/a&gt;: a new framework built with agent experience in mind from the ground up.&lt;/p&gt;
&lt;h2 id=&quot;react-server-components-matured&quot;&gt;React Server Components matured&lt;/h2&gt;
&lt;p&gt;React Server Components (RSCs) have had some ups and downs, after being &lt;a href=&quot;https://legacy.reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html?utm_source=chatgpt.com&quot;&gt;first introduced&lt;/a&gt; five years ago.&lt;/p&gt;
&lt;p&gt;It was a nice promise: What if you could render your site on the server and selectively opt parts of the component tree into client hydration—not just on initial page load, but on all subsequent interactions and navigation?&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://philippeserhal.com/_astro/rsc-josh-comeau.g_M2WEqg.png&quot; alt=&quot;React Server Components by Josh Comeau&quot;&gt;&lt;figcaption&gt;&lt;p&gt;Source:
&lt;a href=&quot;https://www.joshwcomeau.com/react/server-components/&quot;&gt;joshwcomeau.com/react/server-components&lt;/a&gt;&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Years of iteration and false starts followed. A stable implementation didn’t land until mid-2023 with Next.js 13.4. Then came another period of experimentation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shopify built Hydrogen v1 on RSCs, then abandoned them completely with v2&lt;/li&gt;
&lt;li&gt;Remix showed an early prototype (and later pivoted away from React entirely with v3)&lt;/li&gt;
&lt;li&gt;Waku launched as an experimental minimal RSC framework&lt;/li&gt;
&lt;li&gt;The RedwoodJS team spent years working through RSC implementation challenges&lt;/li&gt;
&lt;li&gt;Tanner Linsley’s team quietly tinkered away on RSC support for TanStack Start&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;RSCs in 2025&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In 2025, RSCs reached their next stage of maturity. Maintainers of React, React Router, Waku, Vite, and others came together to collaborate on a shared foundation: &lt;a href=&quot;https://www.npmjs.com/package/@vitejs/plugin-rsc&quot;&gt;@vitejs/plugin-rsc&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It’s no coincidence that in the past year alone:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React 19 shipped with stable RSC support&lt;/li&gt;
&lt;li&gt;Next.js 16 leaned further into RSCs with &lt;a href=&quot;https://nextjs.org/docs/app/getting-started/cache-components&quot;&gt;Cache Components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;React Router 7 launched &lt;a href=&quot;https://remix.run/blog/react-router-and-react-server-components&quot;&gt;RSC support in preview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Parcel shipped native bundler support&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rwsdk.com/&quot;&gt;RedwoodSDK&lt;/a&gt; launched with RSC support on day one&lt;/li&gt;
&lt;li&gt;Waku moved to &lt;a href=&quot;https://waku.gg/blog/waku-v1-alpha&quot;&gt;alpha&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;TanStack Start moved closer than ever to experimental RSC support 👀&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;signals-wonexcept-in-react&quot;&gt;Signals won–except in React&lt;/h2&gt;
&lt;p&gt;As we &lt;a href=&quot;https://www.netlify.com/blog/2024-frameworks-year-in-review/#a-signal-by-any-other-name-would-smell-as-sweet&quot;&gt;discussed last year&lt;/a&gt;, rendering frameworks have rallied around Signals as the basis for reactivity. In 2025, the trend continued:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Angular 20 stabilized its signal APIs&lt;/li&gt;
&lt;li&gt;Angular 21 introduced Signal Forms and made “zoneless” the default&lt;/li&gt;
&lt;li&gt;Vue shipped a beta of &lt;a href=&quot;https://github.com/vuejs/core/releases/tag/v3.6.0-beta.1&quot;&gt;Vapor Mode&lt;/a&gt; (goodbye, Virtual DOM!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Meanwhile, React, virtually (pun intended) the only holdout, dug in further with &lt;a href=&quot;https://react.dev/blog/2025/10/07/react-compiler-1&quot;&gt;React Compiler 1.0&lt;/a&gt;, a band-aid to address shortcomings of its reactivity model.&lt;/p&gt;
&lt;h2 id=&quot;performance-rustification-continued&quot;&gt;Performance ‘Rustification’ continued&lt;/h2&gt;
&lt;p&gt;The ecosystem kept doubling down on step-function performance improvements by moving to Rust (and occasionally Go or Zig).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Next.js and Turbopack:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Next.js 16 promoted Turbopack to stable and made it the default&lt;/li&gt;
&lt;li&gt;Result: 2–5x faster builds for everyone&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rspack.rs/blog/#rspack-joins-the-nextjs-ecosystem&quot;&gt;rspack&lt;/a&gt; also shipped Next.js support (yes, you can now use Next.js with three bundlers: webpack, rspack, and turbopack)&lt;/li&gt;
&lt;li&gt;Vercel added support for the &lt;a href=&quot;https://vercel.com/blog/bun-runtime-on-vercel-functions&quot;&gt;Bun runtime&lt;/a&gt; (Zig!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Vite and VoidZero:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://voidzero.dev/&quot;&gt;VoidZero’s&lt;/a&gt; efforts to rewrite a unified Rollup + esbuild replacement in Rust hit a milestone with the &lt;a href=&quot;https://vite.dev/blog/announcing-vite8-beta&quot;&gt;Vite 8 beta&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Typical improvement: &lt;a href=&quot;https://github.com/vitejs/rolldown-vite-perf-wins&quot;&gt;4–10x faster builds&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;TypeScript in Go:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Microsoft announced in March they were &lt;a href=&quot;https://devblogs.microsoft.com/typescript/typescript-native-port/&quot;&gt;rebuilding TypeScript in Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;By December, they were &lt;a href=&quot;https://devblogs.microsoft.com/typescript/progress-on-typescript-7-december-2025/&quot;&gt;gearing up for production&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Promise: stability and 10x speedups—worth opting in now&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-year-in-brief&quot;&gt;The year in brief&lt;/h2&gt;
&lt;h3 id=&quot;money-moved&quot;&gt;Money moved&lt;/h3&gt;
&lt;p&gt;The framework ecosystem saw significant consolidation and investment in 2025:&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Date&lt;/th&gt;&lt;th&gt;Event&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;March&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://tanstack.com/blog/netlify-partnership&quot;&gt;Netlify sponsored TanStack&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;July&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://vercel.com/blog/nuxtlabs-joins-vercel&quot;&gt;Vercel acquired NuxtLabs&lt;/a&gt; (employer of Nitro and most Nuxt core maintainers)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;September&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://astro.build/blog/mux-official-video-partner/&quot;&gt;Mux&lt;/a&gt;, &lt;a href=&quot;https://astro.build/blog/webflow-official-partner/&quot;&gt;Webflow&lt;/a&gt;, and &lt;a href=&quot;https://astro.build/blog/cloudflare-official-partner/&quot;&gt;Cloudflare&lt;/a&gt; ($150K) sponsored Astro; &lt;a href=&quot;https://blog.cloudflare.com/cloudflare-astro-tanstack/&quot;&gt;Cloudflare sponsored TanStack&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;December&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://bun.com/blog/bun-joins-anthropic&quot;&gt;Anthropic acquired Bun&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;January 2026&lt;/td&gt;&lt;td&gt;&lt;a href=&quot;https://blog.cloudflare.com/astro-joins-cloudflare/&quot;&gt;Cloudflare acquired Astro&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Infrastructure companies are betting on frameworks as a strategic layer. Vercel deepened its Vue ecosystem ties, Cloudflare went all-in on Astro, and Anthropic’s Bun acquisition signals AI companies see JavaScript runtimes as infrastructure worth owning.&lt;/p&gt;
&lt;h3 id=&quot;other-notable-releases&quot;&gt;Other notable releases&lt;/h3&gt;
&lt;p&gt;A few releases didn’t fit the “major framework” category but still shaped the year:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nodejs.org/en/blog/release/v24.0.0&quot;&gt;Node.js 24&lt;/a&gt; (May) – Continued the ESM transition and promoted &lt;code&gt;URLPattern&lt;/code&gt; to a built-in global&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vite.dev/blog/announcing-vite7&quot;&gt;Vite 7&lt;/a&gt; (June) – Incremental improvements ahead of the Rust-powered Vite 8&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nitrojs/nitro/releases/tag/v3.0.1-alpha.0&quot;&gt;Nitro v3 alpha&lt;/a&gt; (October) – The server engine powering Nuxt, AnalogJS, and SolidStart got a ground-up rewrite&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;worth-watching&quot;&gt;Worth watching&lt;/h3&gt;
&lt;p&gt;The honorable mentions that didn’t get their own section; grouped by theme.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Data and caching patterns&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Frameworks got smarter about when to fetch and when to cache. Astro shipped &lt;a href=&quot;https://astro.build/blog/live-content-collections-deep-dive/&quot;&gt;Live Content Collections&lt;/a&gt;, enabling runtime data fetching through the unified Content Layer API—useful for avoiding expensive rebuilds when CMS content changes frequently. Next.js 16 introduced &lt;a href=&quot;https://nextjs.org/docs/app/getting-started/cache-components&quot;&gt;Cache Components&lt;/a&gt;, a stable caching model that consolidates previous experiments (“use cache”, Partial Prerendering, Dynamic IO) into one coherent approach.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Middleware and server boundaries&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The line between “frontend framework” and “backend framework” kept blurring. Next.js &lt;a href=&quot;https://nextjs.org/blog/next-16#proxyts-formerly-middlewarets&quot;&gt;renamed middleware to proxy&lt;/a&gt; after years of confusion about its capabilities—clarifying that it handles routing decisions at the edge, not business logic. They also added Node.js runtime support, addressing a long-standing request.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://remix.run/blog/middleware&quot;&gt;React Router shipped actual middleware&lt;/a&gt;, and SvelteKit introduced Remote Functions: server-only code in &lt;code&gt;.remote.ts&lt;/code&gt; files callable directly from components with full type safety, explicit module boundaries, and &lt;a href=&quot;https://tanstack.com/blog/directives-and-the-platform-boundary&quot;&gt;no JavaScript funny business&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Async patterns and component models&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Svelte introduced &lt;a href=&quot;https://svelte.dev/docs/svelte/await-expressions&quot;&gt;experimental await support&lt;/a&gt; for use directly in components—their answer to React Server Components and &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt;. Meanwhile, Ryan Florence and Michael Jackson unveiled &lt;a href=&quot;https://remix.run/blog/remix-jam-2025-recap&quot;&gt;Remix 3’s radical new direction&lt;/a&gt;: ditching React entirely, embracing micro-packages built on the Web Platform, and returning to imperative &lt;code&gt;this.update()&lt;/code&gt; state management. Their pitch? A baggage-free framework for the LLM era.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ecosystem convergence&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Vite’s dominance continued: &lt;a href=&quot;https://deno.com/blog/fresh-and-vite&quot;&gt;Fresh switched to Vite&lt;/a&gt;, Ember completed its migration, and Angular defaulted to Vitest for testing. &lt;a href=&quot;https://www.netlify.com/blog/2024-frameworks-year-in-review/#what-to-expect-in-2025&quot;&gt;As predicted&lt;/a&gt; last year, Vite’s Environment API saw its first wave of adoption. TanStack Start ships fully on it, Astro 6 will follow, and &lt;a href=&quot;https://reactrouter.com/upgrading/future#futurev8_viteenvironmentapi&quot;&gt;React Router 7.10&lt;/a&gt; and &lt;a href=&quot;https://nuxt.com/blog/v4-2#opt-in-vite-environment-api&quot;&gt;Nuxt 4.2&lt;/a&gt; added opt-in support (stable soon in Nuxt 5).&lt;/p&gt;
&lt;p&gt;Nitro’s role evolved from server engine to &lt;a href=&quot;https://v3.nitro.build/&quot;&gt;Vite plugin&lt;/a&gt;, with TanStack Start &lt;a href=&quot;https://github.com/TanStack/router/discussions/2863#discussioncomment-14052148&quot;&gt;removing it from core&lt;/a&gt; in favor of letting users choose Nitro or alternatives like &lt;a href=&quot;https://docs.netlify.com/build/frameworks/framework-setup-guides/tanstack-start/#deploy-manually&quot;&gt;Netlify’s Vite plugin&lt;/a&gt;. Other Nitro-powered frameworks will &lt;a href=&quot;https://github.com/solidjs/solid-start/discussions/1960&quot;&gt;likely follow&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;OpenTelemetry gained momentum for framework observability: SvelteKit shipped &lt;a href=&quot;https://svelte.dev/blog/sveltekit-integrated-observability&quot;&gt;built-in OTel traces&lt;/a&gt;, Nitro is &lt;a href=&quot;https://github.com/getsentry/sentry-javascript/issues/13670&quot;&gt;working toward support&lt;/a&gt;, and TanStack Start is &lt;a href=&quot;https://tanstack.com/start/latest/docs/framework/react/guide/observability##opentelemetry-integration-experimental&quot;&gt;exploring automatic instrumentation&lt;/a&gt; for server functions, middleware, and route loaders.&lt;/p&gt;
&lt;p&gt;The e18e (Ecosystem Performance) initiative &lt;a href=&quot;https://e18e.dev/blog/the-year-ahead-2026.html&quot;&gt;quietly made everyone’s lives better&lt;/a&gt; by slimming and unifying dependencies across the board and cleaning up the ecosystem at large.&lt;/p&gt;
&lt;p&gt;Finally, TanStack Start quietly added experimental Vue support (alongside React and Solid), leaning further into their framework-agnostic approach.&lt;/p&gt;
&lt;h2 id=&quot;what-to-expect-in-2026&quot;&gt;What to expect in 2026&lt;/h2&gt;
&lt;p&gt;Some reliable predictions for the year ahead:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Angular&lt;/strong&gt; will drop v22 in May and v23 in November (per their &lt;a href=&quot;https://angular.dev/reference/releases#release-frequency&quot;&gt;fixed release schedule&lt;/a&gt;), going deeper into fine-grained reactivity and the Vite ecosystem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Astro 6&lt;/strong&gt; &lt;a href=&quot;https://astro.build/blog/astro-6-beta/&quot;&gt;will ship&lt;/a&gt; with a revamped dev experience and internal cleanup.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Nuxt 5&lt;/strong&gt; will ship shortly after Nitro v3 graduates to stable—with huge &lt;a href=&quot;https://masteringnuxt.com/blog/nitro-v3-whats-coming-with-nuxt-5&quot;&gt;DX and performance gains&lt;/a&gt; from the rebuilt engine. You can &lt;a href=&quot;https://nuxt.com/docs/4.x/getting-started/upgrade#testing-nuxt-5&quot;&gt;try it today&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Waku 1.0&lt;/strong&gt; might graduate from alpha to stable as a minimal RSC alternative.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TanStack Start 1.0&lt;/strong&gt; will presumably launch its own RSC implementation and graduate from RC to stable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Next.js 17&lt;/strong&gt; will likely drop in October. The Next.js team has increased roadmap transparency with partners like Netlify, though this hasn’t translated to much public visibility yet 🤐.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security vulnerabilities&lt;/strong&gt; will continue to target your projects. Keep dependencies up to date with &lt;a href=&quot;https://docs.renovatebot.com/&quot;&gt;Renovate&lt;/a&gt; or &lt;a href=&quot;https://docs.github.com/en/code-security/tutorials/secure-your-dependencies/dependabot-quickstart-guide&quot;&gt;Dependabot&lt;/a&gt;, and follow the &lt;a href=&quot;https://www.netlify.com/changelog/&quot;&gt;Netlify changelog&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Frameworks will keep converging&lt;/strong&gt; &lt;a href=&quot;https://thenewstack.io/google-angular-lead-sees-convergence-in-javascript-frameworks/&quot;&gt;on fundamentals&lt;/a&gt; (though don’t hold your breath for React to ditch its vision and move to Signals).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Coding agents will keep improving&lt;/strong&gt; at web development, and frameworks will keep evolving to &lt;a href=&quot;https://agentexperience.ax/&quot;&gt;meet their needs&lt;/a&gt;. Expect a shift from &lt;a href=&quot;https://modelcontextprotocol.io/&quot;&gt;MCP&lt;/a&gt; to &lt;a href=&quot;https://agentskills.io/home&quot;&gt;Agent Skills&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Phew, you’re all caught up. What are you excited to try this year?&lt;/p&gt;
&lt;p&gt;Stay informed in 2026 by following the &lt;a href=&quot;https://www.netlify.com/changelog/&quot;&gt;Netlify changelog&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title>Frontend frameworks, a 2024 year in review</title><link>https://philippeserhal.com/articles/2024-frameworks-year-in-review/</link><guid isPermaLink="true">https://philippeserhal.com/articles/2024-frameworks-year-in-review/</guid><description>Highlights and emerging trends in 2024, plus what to expect in 2025.</description><pubDate>Fri, 17 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The world of web development is constantly evolving and 2024 was no exception. We know you’re too busy shipping features to keep up with it all.&lt;/p&gt;
&lt;p&gt;Luckily, Netlify is full of web nerds with a passion for building a better web—our Frameworks engineering team has been keeping tabs and taking notes (seriously, you don’t want to know how many Discord servers we’re in). It’s part of how we managed to achieve &lt;a href=&quot;https://www.netlify.com/blog/deploy-nextjs-15/&quot;&gt;day-one support for Next.js 15&lt;/a&gt;, &lt;a href=&quot;https://www.netlify.com/blog/svelte-5-full-support/&quot;&gt;Svelte 5&lt;/a&gt;, &lt;a href=&quot;https://developers.netlify.com/guides/how-to-deploy-angular-18/&quot;&gt;Angular 18&lt;/a&gt;, &lt;a href=&quot;https://astro.build/blog/astro-5/&quot;&gt;Astro 5&lt;/a&gt;, and even &lt;a href=&quot;https://www.netlify.com/blog/platform-primitives-with-nuxt-4/&quot;&gt;pre-release support for Nuxt 4&lt;/a&gt;. So why not share our insights with the community?&lt;/p&gt;
&lt;p&gt;Keep reading for a primer on the trends and plot twists we saw this year, some quick-fire frontend framework news, a dozen releases including some exciting newcomers, and what to expect next year. You’ll be up to speed in no time.&lt;/p&gt;
&lt;h2 id=&quot;frameworks-copied-one-anothers-homework&quot;&gt;Frameworks copied one another’s homework&lt;/h2&gt;
&lt;h3 id=&quot;server-functions-pattern-spreads&quot;&gt;Server Functions pattern spreads&lt;/h3&gt;
&lt;p&gt;Next.js’s &lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations&quot;&gt;Server Actions&lt;/a&gt; provide a &lt;a href=&quot;https://trpc.io/&quot;&gt;tRPC&lt;/a&gt;-like developer experience when crossing the browser-server chasm: call a function on your server from the browser and the framework transparently turns this into a fetch request to your server under the hood, with full type safety.&lt;/p&gt;
&lt;p&gt;This year saw this pattern spread to other full-stack frameworks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React 19 finally shipped stable &lt;a href=&quot;https://react.dev/reference/rsc/server-functions&quot;&gt;Server Functions&lt;/a&gt;, a generalization of Server Actions, via the new &lt;code&gt;&amp;quot;use server&amp;quot;&lt;/code&gt; directive.&lt;/li&gt;
&lt;li&gt;Astro &lt;a href=&quot;https://astro.build/blog/astro-4150/&quot;&gt;shipped stable Actions&lt;/a&gt;, following the same pattern.&lt;/li&gt;
&lt;li&gt;SolidStart 1.0 shipped with support for both &lt;a href=&quot;https://docs.solidjs.com/solid-router/concepts/actions&quot;&gt;Solid Actions&lt;/a&gt; and a &lt;a href=&quot;https://docs.solidjs.com/solid-start/reference/server/use-server&quot;&gt;more general &lt;code&gt;&amp;quot;use server&amp;quot;&lt;/code&gt; directive&lt;/a&gt;. (Although 1.0 only shipped this year, this was actually the very first instance of this pattern!)&lt;/li&gt;
&lt;li&gt;TanStack Start shipped a beta with support for &lt;a href=&quot;https://tanstack.com/router/latest/docs/framework/react/start/server-functions&quot;&gt;Server Functions&lt;/a&gt; that can even be called seamlessly from both the browser (turned into a fetch request) and the server (called as-is).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;component-level-prerendering-goes-mainstream&quot;&gt;Component-level prerendering goes mainstream&lt;/h3&gt;
&lt;p&gt;Historically, there were &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/SSG&quot;&gt;&lt;em&gt;SSG&lt;/em&gt;&lt;/a&gt; sites and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/SSR&quot;&gt;&lt;em&gt;SSR&lt;/em&gt;&lt;/a&gt; sites. A few years ago, many frameworks started supporting hybrid sites, where some routes can be &lt;em&gt;prerendered&lt;/em&gt; (or &lt;em&gt;statically rendered&lt;/em&gt;) and others &lt;em&gt;dynamically rendered&lt;/em&gt;. Next.js’s App Router even does this &lt;a href=&quot;https://nextjs.org/docs/app/building-your-application/caching#full-route-cache&quot;&gt;automatically based on heuristics&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Lately, frameworks have been taking this even further, by allowing for &lt;em&gt;parts&lt;/em&gt; of a page to be prerendered (the &lt;em&gt;static shell&lt;/em&gt;) and the rest to be rendered on the server on the fly. This was popularized by React with &lt;a href=&quot;https://github.com/reactwg/react-18/discussions/37&quot;&gt;&lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; and Streaming SSR&lt;/a&gt;, but it isn’t until recently that meta-frameworks have started to fully implement this in a way that can be directly used by web developers: &lt;a href=&quot;https://remix.run/docs/en/main/guides/streaming&quot;&gt;Remix has supported it&lt;/a&gt; since 2023 and Next.js first announced &lt;a href=&quot;https://nextjs.org/learn/dashboard-app/partial-prerendering&quot;&gt;experimental support for what they called &lt;em&gt;Partial Prerendering (PPR)&lt;/em&gt;&lt;/a&gt; that same year.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://philippeserhal.com/_astro/astro-server-islands.bNWdpkdH.jpg&quot; alt=&quot;A depiction of Astro Server Islands, server-rendered components within a statically rendered shell&quot;&gt;&lt;figcaption&gt;&lt;p&gt;Source:
&lt;a href=&quot;https://astro.build/blog/future-of-astro-server-islands/&quot;&gt;https://astro.build/blog/future-of-astro-server-islands/&lt;/a&gt;&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;This could take up a whole other post (and probably will 👀), but here’s the gist of the 2024 update:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One year after experimental PPR in Next.js 14, &lt;a href=&quot;https://nextjs.org/blog/next-15&quot;&gt;Next.js 15 shipped&lt;/a&gt; without any updates to PPR, as the team wrangles complexities this uncovered.&lt;/li&gt;
&lt;li&gt;React Router 7 shipped with &lt;a href=&quot;https://reactrouter.com/how-to/suspense&quot;&gt;the same functionality&lt;/a&gt; as Remix 2.&lt;/li&gt;
&lt;li&gt;The upstart React-based TanStack Start framework shipped a beta &lt;a href=&quot;https://tanstack.com/router/latest/docs/framework/react/guide/deferred-data-loading&quot;&gt;with the same pattern&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Astro 5 shipped &lt;a href=&quot;https://developers.netlify.com/guides/how-astros-server-islands-deliver-progressive-rendering-for-your-sites/&quot;&gt;Server Islands&lt;/a&gt;, meeting the same needs but with a web-standard implementation allowing for any platform to cache the static shell or even individual dynamic islands.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re wondering why the React Suspense-based approaches aren’t trivially cacheable, &lt;a href=&quot;https://www.youtube.com/watch?v=bFSvD7PVBfQ&quot;&gt;watch this video&lt;/a&gt; and notice how the static shell and the dynamic components are coupled as a single response body—or wait for our upcoming deep-dive post on this!&lt;/p&gt;
&lt;h3 id=&quot;a-signal-by-any-other-name-would-smell-as-sweet&quot;&gt;A signal by any other name would smell as sweet&lt;/h3&gt;
&lt;p&gt;To handle interactivity in the browser without letting web developers handle everything manually (think jQuery) or re-rendering the whole page on every change, frameworks must keep track of the intricate web of dependencies between all the variables in your site’s UI and in your data. This is usually called the framework’s “reactivity” model.&lt;/p&gt;
&lt;p&gt;A framework’s reactivity model is a big part of what makes each framework unique. It manifests itself to you in the framework’s syntax and constraints, molds your mental models, is a major determiner of performance, and—let’s be honest—is a common source of bugs! As an example, you’re likely familiar with React’s reactivity model: Hooks (introduced in 2018 in React 16.8) and, previously, methods like &lt;code&gt;shouldComponentUpdate&lt;/code&gt; and friends.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://philippeserhal.com/_astro/preact-signals.DD6oICeR.png&quot; alt=&quot;A code sample showing Preact Signals in use&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Reactivity models have changed in bursts since the first frameworks in 2010. (For a much deeper dive, start with &lt;a href=&quot;https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf&quot;&gt;A Hands-on Introduction to Fine-Grained Reactivity&lt;/a&gt;, &lt;a href=&quot;https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob&quot;&gt;The Evolution of Signals in JavaScript&lt;/a&gt;, or anything Ryan Carniato of Solid.js has written.) We are witnessing another burst: this year, three major frameworks shipped brand-new reactivity models:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://svelte.dev/blog/svelte-5-is-alive&quot;&gt;Svelte 5&lt;/a&gt; introduced &lt;a href=&quot;https://svelte.dev/blog/runes&quot;&gt;&lt;em&gt;Runes&lt;/em&gt;&lt;/a&gt;, heavily inspired by Solid.js &lt;em&gt;Signals&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.angular.dev/angular-v18-is-now-available-e79d5ac0affe&quot;&gt;Angular 18&lt;/a&gt; introduced experimental &lt;em&gt;zoneless change detection&lt;/em&gt; and &lt;a href=&quot;https://blog.angular.dev/meet-angular-v19-7b29dfd05b84&quot;&gt;Angular 19&lt;/a&gt; continued to iterate on this new &lt;em&gt;Signals&lt;/em&gt;-based reactivity.&lt;/li&gt;
&lt;li&gt;React shipped a beta release of &lt;a href=&quot;https://react.dev/blog/2024/10/21/react-compiler-beta-release&quot;&gt;&lt;em&gt;React Compiler&lt;/em&gt;&lt;/a&gt;, an approach relying on static analysis &lt;em&gt;at build time&lt;/em&gt; of your components to compute dependencies. This approach has been &lt;a href=&quot;https://svelte.dev/docs/svelte/legacy-reactive-assignments&quot;&gt;used by Svelte for years&lt;/a&gt;, but is quite a departure from the more common &lt;em&gt;runtime&lt;/em&gt; reactivity model of most frameworks, &lt;a href=&quot;https://vuejs.org/guide/extras/reactivity-in-depth.html&quot;&gt;like Vue’s&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What’s more, a &lt;a href=&quot;https://github.com/tc39/proposal-signals&quot;&gt;proposal to bring a standard Signal implementation to JavaScript&lt;/a&gt; moved to &lt;a href=&quot;https://tc39.es/process-document/&quot;&gt;stage 1&lt;/a&gt; this year.&lt;/p&gt;
&lt;h3 id=&quot;astro-leveled-the-playing-field&quot;&gt;Astro leveled the playing field&lt;/h3&gt;
&lt;p&gt;Astro is a fairly recent newcomer that has garnered a lot of buzz lately (in the just-released State of JS 2024 report, it &lt;a href=&quot;https://share.stateofjs.com/share/prerendered?localeId=en-US&amp;#38;surveyId=state_of_js&amp;#38;editionId=js2024&amp;#38;blockId=meta_frameworks_ratios%5C%C2%B6ms=%5C%C2%A7ionId=libraries&amp;#38;subSectionId=meta_frameworks&quot;&gt;ranked #1 in interest, retention, and positivity&lt;/a&gt;).&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://philippeserhal.com/_astro/framework-positivity.27QwN9tA.png&quot; alt=&quot;A chart from the State of JS 2024 report showing Astro as the number one framework&quot;&gt;&lt;/figure&gt;
&lt;p&gt;While staying true to its roots as a minimal framework for content-driven sites, it added some key features in 2024 to quiet some critics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Actions, Astro’s answer to Next.js’s Server Functions (more on this above).&lt;/li&gt;
&lt;li&gt;Server Islands, Astro’s answer to Next.js’s Partial Pre-Rendering (more on this above)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://astro.build/blog/future-of-astro-content-layer/&quot;&gt;Content Layer&lt;/a&gt;, Astro’s answer to &lt;a href=&quot;https://www.gatsbyjs.com/docs/reference/graphql-data-layer/&quot;&gt;Gatsby’s GraphQL Data Layer&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Sessions, Astro’s (experimental) storage-agnostic answer to a common meta-framework provision for user session handling (e.g. &lt;a href=&quot;https://remix.run/docs/en/main/utils/sessions&quot;&gt;Remix sessions&lt;/a&gt;, &lt;a href=&quot;https://auth.sidebase.io/&quot;&gt;NuxtAuth&lt;/a&gt;, &lt;a href=&quot;https://next-auth.js.org/&quot;&gt;NextAuth.js&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://astro.build/blog/astro-db/&quot;&gt;Astro DB&lt;/a&gt; and Astro Studio (with &lt;a href=&quot;https://astro.build/blog/goodbye-astro-studio/&quot;&gt;a pivot away from their own SaaS&lt;/a&gt; toward an agnostic layer), Astro’s agnostic layer to interface with relational databases.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;vite-is-even-more-ubiquitous&quot;&gt;Vite is even more ubiquitous&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://vite.dev&quot;&gt;Vite&lt;/a&gt; is a full-featured bundler, compiler, and development server for the web.&lt;/p&gt;
&lt;p&gt;This year, it was &lt;a href=&quot;https://share.stateofjs.com/share/prerendered?localeId=en-US&amp;#38;surveyId=state_of_js&amp;#38;editionId=js2024&amp;#38;blockId=build_tools_ratios%5C%C2%B6ms=%5C%C2%A7ionId=libraries&amp;#38;subSectionId=build_tools&quot;&gt;the most favored option&lt;/a&gt; for web developers using everything from React to Vue, Svelte, or no framework at all. Its focus on performance, out-of-the-box functionality, and limitless configurability has led to a meteoric rise in just a few years.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://philippeserhal.com/_astro/vite-growth.DTAfpaPP.png&quot; alt=&quot;A diagram showing increasing NPM downloads for Vite and numerous logos of frameworks and libraries using&quot;&gt;&lt;figcaption&gt;&lt;p&gt;Source:
&lt;a href=&quot;https://blog.stackblitz.com/posts/what-is-vite-introduction/&quot;&gt;https://blog.stackblitz.com/posts/what-is-vite-introduction/&lt;/a&gt;&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;It is also used under the hood by meta-frameworks like Astro, Nuxt, and SvelteKit—in fact, this duality is a big reason for Vite’s success.&lt;/p&gt;
&lt;p&gt;This year, Remix &lt;a href=&quot;https://remix.run/blog/remix-vite-stable&quot;&gt;shipped support for Vite&lt;/a&gt; (and later &lt;a href=&quot;https://reactrouter.com/upgrading/remix&quot;&gt;dropped support for its own compiler&lt;/a&gt;), Hydrogen &lt;a href=&quot;https://github.com/Shopify/hydrogen/discussions/1987&quot;&gt;switched to Vite&lt;/a&gt;, and even the old-school Ember.js framework is &lt;a href=&quot;https://www.youtube.com/watch?v=Nh6S4cfehs0&quot;&gt;switching to Vite&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This leaves Next.js (Webpack/Turbopack) and Gatsby (Webpack) as the only major front-end meta-frameworks not using Vite.&lt;/p&gt;
&lt;h3 id=&quot;nitro-isnt-just-for-nuxt-anymore&quot;&gt;Nitro isn’t just for Nuxt anymore&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://nitro.build/&quot;&gt;Nitro&lt;/a&gt; is a full-featured server engine library that provides an agnostic development and production server layer for frameworks to build upon, unlocking out-of-the-box support for dozens of deployment targets, such as Node.js, Deno, Netlify Functions, Netlify Edge Functions, Cloudflare Workers, and so on.&lt;/p&gt;
&lt;p&gt;Until recently, it was &lt;a href=&quot;https://nuxt.com/docs/guide/concepts/server-engine&quot;&gt;only used by Nuxt&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This year, &lt;a href=&quot;https://dev.to/analogjs/announcing-analogjs-10-19an&quot;&gt;AnalogJS 1.0 launched&lt;/a&gt;, &lt;a href=&quot;https://www.solidjs.com/blog/solid-start-the-shape-frameworks-to-come&quot;&gt;SolidStart 1.0 launched&lt;/a&gt;, and TanStack Start was announced and quickly &lt;a href=&quot;https://x.com/tannerlinsley/status/1859012733930528803?lang=en&amp;#38;mx=2&quot;&gt;reached beta&lt;/a&gt;. All three of these newcomer frameworks use Vite and Nitro under the hood. What’s more, this combo was repackaged as an intermediary layer called &lt;a href=&quot;https://github.com/nksaraf/vinxi&quot;&gt;Vinxi&lt;/a&gt;—which is used by SolidStart and TanStack Start—significantly lowering the barrier to entry for new frameworks.&lt;/p&gt;
&lt;p&gt;Angular also announced that they are &lt;a href=&quot;https://angular.dev/roadmap#improve-tooling&quot;&gt;exploring switching to Nitro&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;more-compatibility-across-runtimes--platforms&quot;&gt;More compatibility across runtimes &amp;amp; platforms&lt;/h2&gt;
&lt;p&gt;2024 was a great year for compatibility!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://deno.com/blog/v2.0&quot;&gt;Deno 2 launched with full compatibility with Node.js and NPM modules&lt;/a&gt;. All frameworks now run on the Deno runtime!&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.cloudflare.com/builder-day-2024-announcements/#improved-node.js-compatibility-is-now-ga&quot;&gt;The Cloudflare Workers runtime vastly increased its Node.js and NPM module compatibility&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Bun incrementally increased &lt;a href=&quot;https://runtime-compat.unjs.io/&quot;&gt;its Node.js compatibility&lt;/a&gt; throughout the year.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/vercel/next.js/discussions/71727&quot;&gt;Next.js announced upcoming support for the Node.js runtime in middleware&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The Vite 6 &lt;a href=&quot;https://vite.dev/guide/api-environment&quot;&gt;Environment API&lt;/a&gt; shipped, unlocking future improvements to runtime and platform compatibility for a dozen frameworks (more on this below).&lt;/li&gt;
&lt;li&gt;Next.js made strides on openness:
&lt;ul&gt;
&lt;li&gt;The OpenNext initiative spearheaded by SST was joined by &lt;a href=&quot;https://blog.cloudflare.com/builder-day-2024-announcements/#cloudflare-joins-opennext&quot;&gt;Cloudflare&lt;/a&gt; and &lt;a href=&quot;https://www.netlify.com/blog/netlify-joins-opennext/&quot;&gt;Netlify&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://thenewstack.io/vercel-makes-changes-to-next-js-to-simplify-self-hosting/&quot;&gt;The self-hosting docs were augmented&lt;/a&gt; and &lt;a href=&quot;https://nextjs.org/blog/next-15#improvements-for-self-hosting&quot;&gt;Next.js 15 shipped improvements to self-hosting&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Vercel spun out a &lt;a href=&quot;https://github.com/nextjs&quot;&gt;separate Next.js GitHub org with examples for deploying to Vercel competitors&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Node.js &lt;a href=&quot;https://nodejs.org/en/blog/announcements/v22-release-announce#support-requireing-synchronous-esm-graphs&quot;&gt;shipped experimental support for &lt;code&gt;require()&lt;/code&gt; ing ES modules in 22.0.0&lt;/a&gt; and &lt;a href=&quot;https://www.totaltypescript.com/typescript-is-coming-to-node-23&quot;&gt;experimental support for TypeScript syntax in 23.6.0&lt;/a&gt;!&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;developer-experience-kept-improving&quot;&gt;Developer experience kept improving&lt;/h2&gt;
&lt;h3 id=&quot;blazing-fast-rust-based-build-tools-are-almost-here&quot;&gt;Blazing-fast Rust-based build tools are almost here&lt;/h3&gt;
&lt;p&gt;Multiple efforts to write new compilers and bundlers in Rust are underway and made significant progress in 2024:&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://philippeserhal.com/_astro/rspack-vs-webpack.B2atFyD0.png&quot; alt=&quot;A diagram showing webpack with a build time of 6.52s, rspack 0.1 with 0.64&quot;&gt;&lt;figcaption&gt;&lt;p&gt;Source:
&lt;a href=&quot;https://rspack.dev/blog/announcing-1-0&quot;&gt;https://rspack.dev/blog/announcing-1-0&lt;/a&gt;&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://turbo.build/pack/docs&quot;&gt;Turbopack&lt;/a&gt;, Next.js’s Rust rewrite of Webpack, &lt;a href=&quot;https://nextjs.org/blog/turbopack-for-development-stable&quot;&gt;was marked as stable in dev&lt;/a&gt; and will become the default soon.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rspack.dev/&quot;&gt;Rspack&lt;/a&gt;, a straight Rust port of Webpack, &lt;a href=&quot;https://rspack.dev/blog/announcing-1-0&quot;&gt;shipped a stable 1.0&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rolldown.rs/&quot;&gt;Rolldown&lt;/a&gt;, a Rollup-compatible bundler written in Rust, &lt;a href=&quot;https://github.com/rolldown/rolldown/releases/tag/v1.0.0-beta.1&quot;&gt;shipped its first beta on Christmas&lt;/a&gt; and &lt;a href=&quot;https://voidzero.dev/posts/announcing-voidzero-inc&quot;&gt;secured funding to accelerate its development&lt;/a&gt;, along with Vite and &lt;a href=&quot;https://oxc.rs/&quot;&gt;Oxc&lt;/a&gt;. In the near future, &lt;a href=&quot;https://rolldown.rs/guide/#why-rolldown&quot;&gt;Rolldown will replace Rollup and ESBuild to power Vite under the hood&lt;/a&gt; and nearly all frameworks will get an order of magnitude faster builds and dev servers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;upgrades-became-easier-than-ever&quot;&gt;Upgrades became easier than ever&lt;/h3&gt;
&lt;h4 id=&quot;migration-codemods-automate-the-toil&quot;&gt;Migration codemods automate the toil&lt;/h4&gt;
&lt;p&gt;This year, Next.js 15, React Router 7, Astro 5, Nuxt 4, and Svelte 5 all came with an official (sometimes even built-in) upgrade codemod. Most of these were developed and distributed on the impressive &lt;a href=&quot;http://Codemod.com&quot;&gt;Codemod.com platform&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;opt-in-to-breaking-changes-at-your-leisure-with-future-flags&quot;&gt;Opt in to breaking changes at your leisure with “future flags”&lt;/h4&gt;
&lt;p&gt;Gone are the days of choosing between delaying painful upgrades or opting in early to unstable releases. Many libraries nowadays—including to some extent all major frameworks—now follow the “future flag” pattern, where unstable features and breaking changes are shipped incrementally to the current major version, hidden behind a configuration flag. You can explicitly opt into individual flags at your own pace depending on your needs. Notable examples include &lt;a href=&quot;https://docs.astro.build/en/reference/experimental-flags/&quot;&gt;Astro’s experimental flags&lt;/a&gt;, &lt;a href=&quot;https://remix.run/docs/en/main/guides/api-development-strategy&quot;&gt;Remix’s future flags&lt;/a&gt;, and &lt;a href=&quot;https://angular.dev/guide/experimental/zoneless&quot;&gt;Angular’s experimental providers&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://philippeserhal.com/_astro/nuxt-future-flags.DWGFAqmt.png&quot; alt=&quot;A code example showing a nuxt.config.ts file with compatibilityVersion set to 4&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Some libraries like Nuxt take this approach to its fullest potential—you can &lt;a href=&quot;https://nuxt.com/docs/getting-started/upgrade#opting-in-to-nuxt-4&quot;&gt;quite literally opt into Nuxt 4 in Nuxt 3&lt;/a&gt; by setting &lt;code&gt;compatibilityVersion&lt;/code&gt; to &lt;code&gt;4&lt;/code&gt; , which is just toggling a dozen features and breaking changes. Once Nuxt 4 is released, it will consist only of these toggles becoming the default.&lt;/p&gt;
&lt;h3 id=&quot;typechecking-the-elephant-in-the-room&quot;&gt;Typechecking the elephant in the room&lt;/h3&gt;
&lt;p&gt;Although Typescript has been ubiquitous in web development for years now, a small but meaningful corner of web frameworks has remained largely untyped: route params, search params, and cross-references for file-based routes.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://philippeserhal.com/_astro/routing-type-safety.BSSMZSHH.jpg&quot; alt=&quot;A comparison of various aspects of route type safety for a few React-based frameworks&quot;&gt;&lt;figcaption&gt;&lt;p&gt;Source:
&lt;a href=&quot;https://x.com/gill_kyle/status/1835340921535332433&quot;&gt;https://x.com/gill_kyle/status/1835340921535332433&lt;/a&gt;&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;In 2024, we saw upstart framework &lt;a href=&quot;https://tanstack.com/router/v1/docs/framework/react/overview#100-inferred-typescript-support&quot;&gt;TanStack Start&lt;/a&gt; tackle this with a fresh approach that brings 100% end-to-end type safety to file-based routing—and much of it inferred. On the other hand, &lt;a href=&quot;https://reactrouter.com/explanation/type-safety&quot;&gt;Remix successor React Router 7 moved away from fully-file-based routing and introduced a type generation step&lt;/a&gt; in order to achieve similar outcomes. Meanwhile, &lt;a href=&quot;https://nextjs.org/docs/app/api-reference/config/typescript#statically-typed-links&quot;&gt;Next.js&lt;/a&gt;, &lt;a href=&quot;https://nuxt.com/docs/guide/going-further/experimental-features#typedpages&quot;&gt;Nuxt&lt;/a&gt;, and &lt;a href=&quot;https://qwik.dev/docs/labs/typed-routes/&quot;&gt;Qwik&lt;/a&gt; are cooking up their own solutions.&lt;/p&gt;
&lt;h2 id=&quot;emerging-frameworks-pushed-the-ecosystem-forward&quot;&gt;Emerging frameworks pushed the ecosystem forward&lt;/h2&gt;
&lt;p&gt;As made evident by all the movement above, this is a fast-moving space. Much of that is due to innovation and pressure from emerging frameworks. Here’s a quick reference of the ones on our radar this year—and good fodder for your weekend projects in 2025?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://analogjs.org/&quot;&gt;AnalogJS&lt;/a&gt;, an Angular meta-framework (stable since March 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://fresh.deno.dev/&quot;&gt;Deno Fresh&lt;/a&gt;, a Preact-based framework optimized for edge rendering (stable since June 2022)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://htmx.org/&quot;&gt;HTMX&lt;/a&gt;, a return to basics (stable since November 2020)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://onestack.dev/&quot;&gt;One&lt;/a&gt;, an experimental React-based framework (just announced in October 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://redwoodjs.com/&quot;&gt;RedwoodJS&lt;/a&gt;, a batteries-included full-stack React-based framework (stable since April 2022)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://start.solidjs.com/&quot;&gt;SolidStart&lt;/a&gt;, a Solid.js meta-framework (stable since May 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tanstack.com/start/latest&quot;&gt;TanStack Start&lt;/a&gt;, a new React meta-framework (in beta since December 2024)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://qwik.dev/&quot;&gt;Qwik&lt;/a&gt;, a novel framework that introduces &lt;em&gt;resumability&lt;/em&gt; instead of hydration (stable since May 2023)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;news&quot;&gt;News&lt;/h2&gt;
&lt;h3 id=&quot;major-releases&quot;&gt;Major releases&lt;/h3&gt;
&lt;p&gt;These are the (eleven!) major front-end framework releases that happened this year:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;March 14: &lt;a href=&quot;https://dev.to/analogjs/announcing-analogjs-10-19an&quot;&gt;AnalogJS 1.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;May 21: &lt;a href=&quot;https://www.solidjs.com/blog/solid-start-the-shape-frameworks-to-come&quot;&gt;SolidStart 1.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;May 22: &lt;a href=&quot;https://developers.netlify.com/guides/how-to-deploy-angular-18/&quot;&gt;Angular 18&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;September 6: &lt;a href=&quot;https://redwoodjs.com/upgrade/v8&quot;&gt;RedwoodJS 8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;October 2: &lt;a href=&quot;https://www.11ty.dev/blog/eleventy-v3/&quot;&gt;Eleventy 3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;October 19: &lt;a href=&quot;https://www.netlify.com/blog/svelte-5-full-support/&quot;&gt;Svelte 5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;October 21: &lt;a href=&quot;https://nextjs.org/blog/next-15&quot;&gt;Next.js 15&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;November 19: &lt;a href=&quot;https://blog.angular.dev/meet-angular-v19-7b29dfd05b84&quot;&gt;Angular 19&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;November 22: &lt;a href=&quot;https://developers.netlify.com/guides/how-to-deploy-a-react-router-7-site-to-netlify/&quot;&gt;React Router 7 (aka Remix 3)&lt;/a&gt; (ICYMI: on May 15, &lt;a href=&quot;https://remix.run/blog/merging-remix-and-react-router&quot;&gt;Ryan Florence dropped a bomb at React Conf by announcing the Remix framework and the React Router library would merge and the Remix brand would “take a nap”&lt;/a&gt;!)&lt;/li&gt;
&lt;li&gt;December 3: &lt;a href=&quot;https://astro.build/blog/astro-5/&quot;&gt;Astro 5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;December 5: &lt;a href=&quot;https://react.dev/blog/2024/12/05/react-19&quot;&gt;React 19&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some other projects had notable releases as well:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;April 24: &lt;a href=&quot;https://nodejs.org/en/blog/announcements/v22-release-announce&quot;&gt;Node.js 22&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;August 28: &lt;a href=&quot;https://rspack.dev/blog/announcing-1-0&quot;&gt;Rspack 1.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;October 3: &lt;a href=&quot;https://x.com/stackblitz/status/1841873251313844631&quot;&gt;bolt.new&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;October 9: &lt;a href=&quot;https://deno.com/blog/v2.0&quot;&gt;Deno 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;November 26: &lt;a href=&quot;https://vite.dev/blog/announcing-vite6&quot;&gt;Vite 6&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;sponsorship-and-funding-changes&quot;&gt;Sponsorship and funding changes&lt;/h3&gt;
&lt;p&gt;Some key projects announced partnership or funding news:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;March 26: &lt;a href=&quot;https://www.builder.io/blog/qwik-next-leap&quot;&gt;Builder.io spun out Qwik as a community-owned project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;April 24: &lt;a href=&quot;https://www.builder.io/blog/builder-closes-20-million-funding-m12-microsoft&quot;&gt;Builder.io (maintainers of Qwik) raised $20M in funding&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;July 15: &lt;a href=&quot;https://astro.build/blog/netlify-official-deployment-partner/&quot;&gt;Astro partnered with Netlify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;September 30: &lt;a href=&quot;https://voidzero.dev/posts/announcing-voidzero-inc&quot;&gt;Evan You founded VoidZero&lt;/a&gt;, a VC-backed company, to provide funding ($4.6M) and stability to Vite, Vitest, Rolldown, and OXC&lt;/li&gt;
&lt;li&gt;December 2: &lt;a href=&quot;https://astro.build/blog/idx-official-online-editor-partner/&quot;&gt;Astro partnered with Google&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;honorable-mentions&quot;&gt;Honorable mentions&lt;/h2&gt;
&lt;figure&gt;&lt;img src=&quot;https://philippeserhal.com/_astro/multi-flight.CnAPY6pP.png&quot; alt=&quot;A depiction of fetch single-flighting&quot;&gt;&lt;figcaption&gt;&lt;p&gt;Source:
&lt;a href=&quot;https://www.solidjs.com/blog/solid-start-the-shape-frameworks-to-come&quot;&gt;https://www.solidjs.com/blog/solid-start-the-shape-frameworks-to-come&lt;/a&gt;&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Frameworks showed renewed interest in single-flighting requests from the browser: &lt;a href=&quot;https://remix.run/docs/en/main/guides/single-fetch&quot;&gt;Remix introduced Single Fetch&lt;/a&gt; (now the default in its successor React Router 7), &lt;a href=&quot;https://www.solidjs.com/blog/solid-start-the-shape-frameworks-to-come&quot;&gt;SolidStart 1.0 shipped with Single Flight Mutations&lt;/a&gt;, and &lt;a href=&quot;https://x.com/AdamRackis/status/1869230195699499030&quot;&gt;TanStack Start is working on the same&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.angular.dev/angular-v18-is-now-available-e79d5ac0affe#6226&quot;&gt;Angular shipped &lt;em&gt;event replay&lt;/em&gt;&lt;/a&gt;: retroactively applying user interactions that occurred on parts of the page that hadn’t yet been made interactive. It remains to be seen if others will follow suit. In the meantime, &lt;a href=&quot;https://qwik.dev/docs/concepts/resumable/&quot;&gt;Qwik sidesteps the problem entirely by designing for &lt;em&gt;resumability&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Astro stole our hearts by &lt;a href=&quot;https://astro.build/blog/future-of-astro-zero-js-view-transitions/&quot;&gt;proactively collaborating with the ecosystem to improve the web platform when it found it had reached its limits&lt;/a&gt;, making the web better for everyone in the process.&lt;/p&gt;
&lt;p&gt;Much of the web development community suddenly migrated to &lt;a href=&quot;https://bsky.app/&quot;&gt;Bluesky&lt;/a&gt; at the end of 2024.&lt;/p&gt;
&lt;h2 id=&quot;what-to-expect-in-2025&quot;&gt;What to expect in 2025&lt;/h2&gt;
&lt;figure&gt;&lt;img src=&quot;https://philippeserhal.com/_astro/vite-env-api.CoA2cjAR.png&quot; alt=&quot;A depiction of the Vite Environment API&apos;s architecture&quot;&gt;&lt;figcaption&gt;&lt;p&gt;Source:
&lt;a href=&quot;https://vite.dev/guide/api-environment&quot;&gt;https://vite.dev/guide/api-environment&lt;/a&gt;&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;If you’re looking for bold predictions for 2025, look elsewhere. Here are some sure bets we expect in the next year:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Angular releases occur on a &lt;a href=&quot;https://angular.dev/reference/releases#support-policy-and-schedule&quot;&gt;fixed schedule&lt;/a&gt;, so expect to see Angular 20 in May and Angular 21 in November. Check out the &lt;a href=&quot;https://angular.dev/roadmap&quot;&gt;Angular roadmap&lt;/a&gt; for hints on what will ship.&lt;/li&gt;
&lt;li&gt;Node.js 18 will reach End of Life (EOL) status in April. That only leaves you four months to upgrade your apps. (&lt;a href=&quot;https://answers.netlify.com/t/builds-functions-plugins-default-node-js-version-upgrade-to-22/135981&quot;&gt;Upgrade your Netlify builds and functions to Node.js 22 now!&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Nuxt 4—which has been all but ready for months as of writing—will go stable. (&lt;a href=&quot;https://www.netlify.com/blog/platform-primitives-with-nuxt-4/&quot;&gt;Opt in early on Netlify now.&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The &lt;a href=&quot;https://gitnation.com/contents/whats-new-in-vite-6&quot;&gt;Vite Environment API&lt;/a&gt;, shipped in Vite 6 at the end of 2024, will lead to great improvements in local dev experience for many Vite-based frameworks, particularly in terms of stability, production parity, and runtime compatibility. (Expect developing Netlify sites to get even simpler 👀.)&lt;/li&gt;
&lt;li&gt;TanStack Start, after shipping a beta release at the end of 2024, will ship a Release Candidate in 2025—maybe even a 1.0?&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/QwikDev/qwik/releases/tag/%40qwik.dev%2Fcore%40v2.0.0-alpha.0&quot;&gt;Qwik 2.0&lt;/a&gt; will be released.&lt;/li&gt;
&lt;li&gt;TanStack and Netlify will cook something up 👀.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;closing-remarks&quot;&gt;Closing remarks&lt;/h2&gt;
&lt;p&gt;Whew, you’re all caught up. Stay informed in the new year by following the &lt;a href=&quot;https://www.netlify.com/changelog/&quot;&gt;Netlify Changelog&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;What will you build this year?&lt;/p&gt;</content:encoded></item><item><title>❝Every now and then I get a random Access Denied page...❞</title><link>https://philippeserhal.com/articles/random-access-denied-plot-twist/</link><guid isPermaLink="true">https://philippeserhal.com/articles/random-access-denied-plot-twist/</guid><description>Nondeterminism, library bugs, race conditions, incorrect docs, a behaviour that shouldn&apos;t be possible, and an unbelievable twist.</description><pubDate>Thu, 01 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;We received a bug report from one of our customer service associates (let’s call her Sarah) that went something like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When I’m using [internal customer service web app], every now and then I get this “Access Denied” page.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I fire off some follow-up questions and start checking the usual suspects: Sarah’s user has the expected roles, her browser is supported and up to date, and so on.&lt;/p&gt;
&lt;p&gt;I open up our error tracking service, &lt;a href=&quot;https://rollbar.com/&quot;&gt;Rollbar&lt;/a&gt;, and find some recent occurrences of HTTP 403 errors for Sarah in this app, coming from the backend service.&lt;/p&gt;
&lt;p&gt;It isn’t obvious why a 403 would be returned – according to Rollbar, Sarah’s authentication token is valid, her user has the expected roles, and the resources being accessed should be accessible to her.&lt;/p&gt;
&lt;h2 id=&quot;upon-closer-inspection&quot;&gt;Upon closer inspection&lt;/h2&gt;
&lt;p&gt;I try to reproduce this locally. I spin up development instances of the backend and frontend, and click around. I load up similar pages. I log in as a user with the same privileges as Sarah. No errors.&lt;/p&gt;
&lt;p&gt;When I go back to Rollbar, something catches my eye:&lt;/p&gt;
&lt;pre class=&quot;astro-code plastic&quot; style=&quot;background-color:#21252B;color:#A9B2C3;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;firstName&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;Sarah&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;lastName&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;Lastname&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;email&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;sarah.lastname@example.com&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;roles&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;customer_service&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;manager&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;tempWorker&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;********&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What’s this ”********” about? I vaguely recall having seen (and promptly ignored) this string in Rollbar details before, but I’d never thought much of it.&lt;/p&gt;
&lt;p&gt;It occurs to me at this point that &lt;code&gt;tempWorker&lt;/code&gt; is a field involved in authorization and I’m trying to explain mysterious “access denied” errors. It certainly seems like a plausible connection. Is this just a red herring? Is this even worth pursuing?&lt;/p&gt;
&lt;p&gt;I check all the other 403 occurrences for Sarah: they all show ”********”.&lt;/p&gt;
&lt;p&gt;I check occurrences of other, non-403 errors for Sarah: they all show &lt;code&gt;&amp;quot;tempWorker&amp;quot;: false&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I check 403 occurrences for other users: they all show ”********” as well.&lt;/p&gt;
&lt;p&gt;In the meantime, Sarah has responded to my follow-up questions and let me know that “this happens randomly, rarely, to everyone” and they’ve learned to just “log out and back in and that usually fixes it.”&lt;/p&gt;
&lt;p&gt;It seems we’re on to something here. It seems unlikely, but all signs point to this &lt;code&gt;false&lt;/code&gt; unexpectedly being ”********”, rarely, and randomly, and this being either the cause of — or correlated with — the issue.&lt;/p&gt;
&lt;p&gt;Okay. Where do we go from here?&lt;/p&gt;
&lt;h2 id=&quot;auth-overview&quot;&gt;Auth overview&lt;/h2&gt;
&lt;p&gt;I remind myself how authentication and authorization work in this Node.js service:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Authenticated requests from the browser are sent with a user cookie, containing a signed JSON Web Token (JWT).&lt;/li&gt;
&lt;li&gt;When handling a request, the Express.js web server runs an auth middleware that decodes the JWT, verifies its signature and expiry, and populates a hydrated user session object (containing fields like email, roles, tempWorker, etc.) from the resulting payload as &lt;code&gt;req.user&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Before accessing a resource, downstream middlewares use values from &lt;code&gt;req.user&lt;/code&gt; to check the user’s permissions against the resource. When a check fails, we return a 403.&lt;/li&gt;
&lt;li&gt;Finally, before sending the response, the auth middleware from step 2 checks if &lt;code&gt;req.user&lt;/code&gt; has been changed. If so, it set a &lt;code&gt;Set-Cookie&lt;/code&gt; response header so that the browser can store an updated JWT.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Working backwards from this, I reasoned that if something were to overwrite &lt;code&gt;req.user.tempWorker&lt;/code&gt; during the request handling lifecycle, the new value would be saved by the browser and sent to the server on subsequent requests. Sarah also reported that logging out and back in is the only workaround, which aligns with this hypothesis.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Insight #1:&lt;/strong&gt; In-memory corruption of the JWT payload may only be reflected on subsequent requests.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now, remember step 3 above? Ultimately, somewhere there’s a bit of code along the lines of:&lt;/p&gt;
&lt;pre class=&quot;astro-code plastic&quot; style=&quot;background-color:#21252B;color:#A9B2C3;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;req&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;tempWorker&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B57EDC&quot;&gt;sendStatus&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#56B6C2&quot;&gt;403&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simply put, users who are “temp workers” can’t access anything in this app.&lt;/p&gt;
&lt;p&gt;Wait — let’s look at that again and fill in the value we’re seeing:&lt;/p&gt;
&lt;pre class=&quot;astro-code plastic&quot; style=&quot;background-color:#21252B;color:#A9B2C3;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;********&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B57EDC&quot;&gt;sendStatus&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#56B6C2&quot;&gt;403&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In JavaScript, nonempty strings evaluate to true. I’ll spare you the embarrassing amount of time it took me to make that connection. This code and I were both expecting this field to always be a boolean.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Insight #2:&lt;/strong&gt; Any truthy value for &lt;code&gt;tempWorker&lt;/code&gt; would result in a 403… and ”********” is truthy.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;these--asterisks&quot;&gt;These ******** asterisks&lt;/h2&gt;
&lt;p&gt;Okay, we have a working theory. But where are these asterisks coming from?&lt;/p&gt;
&lt;p&gt;After clicking around in Rollbar, I see something like this and the answer is immediately obvious:&lt;/p&gt;
&lt;pre class=&quot;astro-code plastic&quot; style=&quot;background-color:#21252B;color:#A9B2C3;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;apiToken&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;********&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is Rollbar’s mechanism for scrubbing sensitive information!&lt;/p&gt;
&lt;p&gt;What’s going on here? We still have two big mysteries to solve:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Why would a field called &lt;code&gt;tempWorker&lt;/code&gt; be scrubbed?&lt;/li&gt;
&lt;li&gt;Why would this cause issues in our app if scrubbing is a downstream transformation at the Rollbar reporting level?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We’ve arrived at the most unbelievable part of the story. Pause here, and try to guess. (Good luck.)&lt;/p&gt;
&lt;h2 id=&quot;the-tempworker-mystery-solved&quot;&gt;The “tempWorker” mystery, solved&lt;/h2&gt;
&lt;p&gt;Give up?&lt;/p&gt;
&lt;p&gt;The answer to the first question occurred to me suddenly, lying in bed later that day. The question needed to be reframed: What was special about the string “tempWorker” but not “email” or “roles” if you’re trying to scrub sensitive fields?&lt;/p&gt;
&lt;p&gt;tem&lt;strong&gt;pW&lt;/strong&gt;orker&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;pw&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Oh &lt;em&gt;no&lt;/em&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Insight #3:&lt;/strong&gt; “pw” is a common abbreviation of “password”.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Could Rollbar be checking, case-insensitively for the substring “pw” anywhere in a field name and scrubbing the corresponding value?&lt;/p&gt;
&lt;p&gt;This must not come up often. You don’t see “turnipwood” or “shipwright” very often in a JSON key.&lt;/p&gt;
&lt;p&gt;I checked the &lt;a href=&quot;https://github.com/rollbar/rollbar.js/blob/f0140a050c270981cfb980c9aa492a2d032fcb2f/README.md#context-1&quot;&gt;rollbar.js documentation on scrubbing&lt;/a&gt; and found this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;scrubFields&lt;/strong&gt;
A list containing names of keys/fields/query parameters to scrub. Scrubbed fields will be normalized to all &lt;code&gt;*&lt;/code&gt; before being reported to Rollbar. This is useful for sensitive information that you do not want to send to Rollbar. e.g. User tokens&lt;/p&gt;
&lt;p&gt;Default: &lt;code&gt;[&amp;quot;passwd&amp;quot;, &amp;quot;password&amp;quot;, &amp;quot;secret&amp;quot;, &amp;quot;confirm_password&amp;quot;, &amp;quot;password_confirmation&amp;quot;]&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Though this confirms the scrubbing happens in the Rollbar SDK (“client side”), “pw” was not listed and there’s no mention of substring matching. I almost abandoned this hypothesis at this point.&lt;/p&gt;
&lt;p&gt;Luckily, I decided to check the library’s source code for good measure and &lt;a href=&quot;https://github.com/rollbar/rollbar.js/blob/f0140a050c270981cfb980c9aa492a2d032fcb2f/package.json#L135-L161&quot;&gt;there it was&lt;/a&gt;… “pw” and 30 other undocumented patterns:&lt;/p&gt;
&lt;pre class=&quot;astro-code plastic&quot; style=&quot;background-color:#21252B;color:#A9B2C3;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;scrubFields&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;pw&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;pass&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;passwd&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;password&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;password_confirmation&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5F6672;font-style:italic&quot;&gt;  /* .... */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The docs were lying to me! What else might they be lying about? I pulled up the scrubbing code and found &lt;a href=&quot;https://github.com/rollbar/rollbar.js/blob/f0140a050c270981cfb980c9aa492a2d032fcb2f/src/utility.js#L550&quot;&gt;this regular expression&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code plastic&quot; style=&quot;background-color:#21252B;color:#A9B2C3;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#B57EDC&quot;&gt; RegExp&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#56B6C2&quot;&gt;\\\\&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;[?(%5[bB])?&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt; scrubFields&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#56B6C2&quot;&gt;\\\\&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;[?(%5[bB])?&lt;/span&gt;&lt;span style=&quot;color:#56B6C2&quot;&gt;\]&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;?(%5[dD])?&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Uhh. What? Let’s simplify this, using “pw” as an example:&lt;/p&gt;
&lt;pre class=&quot;astro-code plastic&quot; style=&quot;background-color:#21252B;color:#A9B2C3;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;\\\[&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;b&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;pw&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;\\\\[&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;b&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;\]&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;%&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;d&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;?/&lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What are &lt;code&gt;%5b&lt;/code&gt; and &lt;code&gt;%5c&lt;/code&gt;? Let’s check with a browser console:&lt;/p&gt;
&lt;pre class=&quot;astro-code plastic&quot; style=&quot;background-color:#21252B;color:#A9B2C3;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#B57EDC&quot;&gt; decodeURIComponent&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;%5b&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E06C75&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#B57EDC&quot;&gt; decodeURIComponent&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;%5d&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OK, let’s break it down then:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;\\\[?&lt;/code&gt; ← 0 or 1 left bracket&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(%5b)?&lt;/code&gt; ← 0 or 1 encoded left bracket&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pw&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\\\[?&lt;/code&gt; ← 0 or 1 left bracket&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(%5b)?&lt;/code&gt; ← 0 or 1 encoded left bracket&lt;/li&gt;
&lt;li&gt;&lt;code&gt;\]?&lt;/code&gt; ← 0 or 1 right bracket&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(%5d)?&lt;/code&gt; ← 0 or 1 encoded right bracket&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most of this appears to be attempting to capture the full field name, but these capture groups aren’t used… Let’s move on.&lt;/p&gt;
&lt;p&gt;The takeaway is that this matches strings that contain “pw” anywhere. This uncovers a second inaccuracy in the docs.&lt;/p&gt;
&lt;h2 id=&quot;narrowing-in&quot;&gt;Narrowing in&lt;/h2&gt;
&lt;p&gt;At this point, two mysteries remained:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Why would the Rollbar reporting module’s scrubbing mechanism affect our application’s behaviour at all? Shouldn’t it only affect what ends up in Rollbar?&lt;/li&gt;
&lt;li&gt;Why does this bug occur, as Sarah described it, “randomly”?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It turns out these questions are related.&lt;/p&gt;
&lt;p&gt;Rollbar is an error reporting tool. So, when does an error get reported to Rollbar from this service? There are a few scenarios (logged error, uncaught exception, unhandled rejection, etc.), but let’s focus on the happy path: an error has been logged via our logging module:&lt;/p&gt;
&lt;pre class=&quot;astro-code plastic&quot; style=&quot;background-color:#21252B;color:#A9B2C3;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;logger&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B57EDC&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;({ foo: &lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#98C379&quot;&gt;extra data&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt; }, &lt;/span&gt;&lt;span style=&quot;color:#C6CCD7&quot;&gt;err&lt;/span&gt;&lt;span style=&quot;color:#A9B2C3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this service, a request logging middleware automatically logs an error whenever a request responds with a 5xx status code.&lt;/p&gt;
&lt;p&gt;In this logging module configuration, API requests to Rollbar are nonblocking. In the context of a request our service is handling, this is asynchronous, running in the background, and does not block our delivery of a response.&lt;/p&gt;
&lt;p&gt;So, at last, here’s the final puzzle piece solved:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Insight #4:&lt;/strong&gt; When an error is “logged” in a request handler, the actual logging side effects nondeterministically occur either before or after the response is delivered.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It’s a race condition:&lt;/p&gt;
&lt;pre class=&quot;astro-code plastic&quot; style=&quot;background-color:#21252B;color:#A9B2C3;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;|------------- Request lifecycle -------------|&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|^ Start handling request                     |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|         ^ &amp;quot;Log&amp;quot; an error                    |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|                                             |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Scenario 1:                                   |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|            ^ Finish flushing the error      |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|                 ^ Send request response     |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Scenario 2:                                   |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|            ^ Send request response          |&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|                 ^ Finish flushing the error |&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And it only matters because…&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💡 Insight #5:&lt;/strong&gt; rollbar.js mutates error payloads!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It was obvious from the observations above, but I easily confirmed this by &lt;a href=&quot;https://github.com/rollbar/rollbar.js/blob/f0140a050c270981cfb980c9aa492a2d032fcb2f/src/utility.js#L156&quot;&gt;reading the source code&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To summarize: when a request is about to respond with a 5xx status code, an error is logged, and a race condition occurs — if the response is delivered first, great; but if the error payload is scrubbed first (right before being sent to the Rollbar API), this scrubbing &lt;strong&gt;mutates&lt;/strong&gt; &lt;code&gt;req.user&lt;/code&gt; such that when the response is about to be delivered, a frivolous and corrupted &lt;code&gt;Set-Cookie&lt;/code&gt; header is attached to the response.&lt;/p&gt;
&lt;p&gt;When this occurs, the browser updates its cookie with the updated payload. On subsequent requests, this corrupted cookie is sent along via the &lt;code&gt;Cookie&lt;/code&gt; header. The JWT in the cookie is perfectly valid, correctly encoded, and cryptographically signed with the service’s secret, so it’s validated, decoded, and parsed by the service and used to populate &lt;code&gt;req.user&lt;/code&gt;. The auth middleware sees a truthy &lt;code&gt;req.user.tempWorker&lt;/code&gt; (the string ”********”) and sends a 403.&lt;/p&gt;
&lt;p&gt;Case closed.&lt;/p&gt;
&lt;h2 id=&quot;the-fix&quot;&gt;The fix&lt;/h2&gt;
&lt;p&gt;“Fixing” the bug was the boring part. I applied a temporary fix to clone the user object before logging it, and I opened three rollbar.js issues (&lt;a href=&quot;https://github.com/rollbar/rollbar.js/issues/508&quot;&gt;one&lt;/a&gt;, &lt;a href=&quot;https://github.com/rollbar/rollbar.js/issues/509&quot;&gt;two&lt;/a&gt;, &lt;a href=&quot;https://github.com/rollbar/rollbar.js/issues/507&quot;&gt;three&lt;/a&gt;).&lt;/p&gt;
&lt;h2 id=&quot;lessons-learned&quot;&gt;Lessons learned&lt;/h2&gt;
&lt;p&gt;What are my takeaways from this story? I have only some scattered thoughts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When investigating difficult bugs, you often need to ignore your instincts. Your instincts tell you “X should work this way” and gloss over X, and “Y should work that way” and gloss over Y, and pretty soon you’re glossing over everything; but something, somewhere isn’t working how it’s intended. Ignore your instincts and follow only the evidence. Write down or say out loud only what you truly, truly know for certain, and draw only logical conclusions from there — or, identify areas where evidence is still missing, devise a way to gather it, and repeat.&lt;/li&gt;
&lt;li&gt;In general, it’s safe to assume the bug is likely in your source code. Occasionally, it’s in a library. Sometimes though, it’s three library bugs wearing a race-condition trench coat. Sometimes.&lt;/li&gt;
&lt;li&gt;None of this would have been possible with a robust type system… but that’s a post for another day.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thanks for reading.&lt;/p&gt;</content:encoded></item></channel></rss>