After two decades of writing web apps – most of it in Symfony and Vue – I got tired of paying the headless tax. Reactolith is the small piece of infrastructure that let me stop.
I've been writing web apps for the better part of two decades – first as a developer in various agencies, and then running my own software studio for the last eight years. That's long enough to watch architecture trends come, go, and come back around again, sometimes with a new name and sometimes with the same one.
Most of that time I worked with Symfony, and before that Ruby on Rails. I fell in love with full-stack frameworks because they gave you structure for free. There was no real debate about where forms belonged, where validation lived, or how an authentication flow should be wired up – the framework had already made those decisions for you. When a new developer joined the agency, they didn't need a week to find their bearings. They opened the project, recognised the conventions, and knew where to write their code. That kind of shared mental model is a property a lot of modern stacks have quietly lost, and I think it's underrated how much it actually matters for shipping software.
Then React and Vue happened, and the rules changed. Clients wanted highly interactive interfaces, and rendering everything through Twig stopped being enough on its own. So in the agency we did what almost everyone did during those years – we compromised. The honest truth is that I never really liked either of the two compromises we settled into, and after a while it started to feel like the entire industry had just accepted them as the cost of doing business.
The two compromises I lived with for years
The first approach was the islands pattern: keep the Symfony backend, render most of the page through normal templates, and mount isolated Vue components into specific spots where you needed interactivity. It works well enough on a marketing page or a simple dashboard. But the moment two components on the same page need to share state, or one needs to know what the other is doing, the model breaks down. You start reaching for global stores, custom event buses, or – and this is when I knew we were in trouble – passing data around through window. It isn't really full-stack frontend development. It's a component framework stapled onto a server-rendered template, and it severely limits what these tools were actually designed to do.
The second approach became our agency default at some point: go fully headless. Symfony in one repo, a Nuxt or Next frontend in another, and an API layer holding the two together. It works. It scales. It's the path most teams take today, and for good reasons. But the overhead is real, and a lot of it is accidental rather than essential. You're now running two projects, with two CI pipelines, two deployment stories, and two sets of dependencies to keep up to date. And you have to build an API for everything, even the parts that are only ever consumed in one specific place. Designing, securing, and maintaining a proper endpoint for something like a password reset or an internal admin toggle is just busywork – work you're doing not because the feature requires it, but because the architecture does. An MVP that should have been a weekend project quietly turns into a small monorepo, and you spend more time on plumbing than on the product.
I kept coming back to the same thought: if we could just build this full-stack again, everything would be so much simpler.
Where the idea actually came from
The push came from an unexpected direction – Hotwire Turbo, the toolkit the Rails community has been quietly using and refining for years now. The philosophy is the part that's worth stealing, regardless of which language you write in. The backend stays in charge of state. The user does something, the backend computes the next state, sends it down to the browser, and a morphing algorithm patches only the parts of the DOM that actually changed. No client-side router, no duplicated state, no API layer in the middle. Just the server doing what servers are good at, and the browser doing the minimum it needs to.
When I really sat with that for a while, I had the same reaction a lot of developers have when they first see it work properly: this is how I want to build web apps. The problem was that I didn't want to give up the productivity of a modern component framework or the declarative composition I'd gotten used to over the years. I wanted Turbo's workflow, but with a real component model on top.
I'd worked almost exclusively in Vue up to that point, and honestly I'd been pretty happy there. But while exploring this idea I kept running into the same realisation about React: its reconciler already does the exact job that idiomorph does for HTML, just one level up the stack. If you send a new component tree from the server, React will diff it against what's currently mounted and surgically update only what changed. Form state is preserved. Focus is preserved. Scroll positions stay where they were. You don't need a client-side router, you don't need a state management library, and you don't need to design an API for things that only happen in one place. The reconciler is, in effect, already a diff engine – you just have to feed it from the server instead of from client-side state transitions.
That, combined with how mature the React ecosystem has become – shadcn, the tooling, the sheer breadth of libraries you can pull in without a second thought – was what finally got me to switch this year. Reactolith is the project that made the switch worth it.
What it actually is
Reactolith is a thin layer that lets you build monolithic web applications where the backend drives state and the frontend is fully React. The server emits the component tree, React reconciles it on the client, and you get the developer experience of a classic full-stack framework together with the UI capabilities of a modern single-page app. There's no headless split to maintain, no API to design for internal-only features, and no two-repo dance every time you want to ship a small change.
It's deliberately a small project, and I want it to stay that way. It isn't a UI framework, a state manager, or a backend framework – shadcn, React, and Symfony already do those jobs well. What Reactolith does is the narrow, specific job of translating server output into a React tree, intercepting navigation, and keeping the two sides in sync. In spirit it's closer to something like morphdom or idiomorph than it is to Next, Remix, or Inertia.
Who it's for
Reactolith is built for the kind of team where the people working on a given product can still fit around a table – which, in my experience, is most teams, even inside fairly large companies. Small product teams, agencies, indie hackers, full-stack developers who wear multiple hats. The setup where two or three or five people own a stack together, care about code they'll still understand in eighteen months, and would rather spend their time on the product than on integration plumbing between two codebases.
The place where Reactolith stops being the right answer is at genuine scale – the kind of organisation with twenty backend engineers and forty frontend engineers working on the same product. At that point you end up splitting teams along an API boundary anyway, and the overhead of going fully headless starts paying for itself in pure coordination cost. That's a different kind of problem with a different kind of solution. But that describes far fewer teams than the discourse around modern web architecture sometimes suggests.
Try it
The project is live. I'm using it in production for my own work, and I'd genuinely love feedback from anyone building in this corner of the stack – especially the people who've felt the same friction over the years and were quietly hoping someone would put something like this together.
Let's make full-stack feel good again.
This article was originally published on Medium.
Top comments (0)