Learnings using Phoenix LiveView for Internal Web Applications

Joel Kemp
Elixir Learnings
Published in
8 min readJun 24, 2021

Disclaimer: Opinions expressed are solely my own and do not express the views or opinions of my employer.

TL;DR: After building a large internal web application, I found that Phoenix LiveView’s programming model lets you build web applications in half the time and with half the number of people — compared to a single page application with a JSON API backend. Developers without frontend experience are also empowered to contribute given the fewer moving parts with this model. Aside from the programming model, LiveView itself lets you build fault-isolated components that keep the rest of your page usable with no extra code.

Context

Internal web apps tend to have some of the following qualities: they’re important, but not customer facing; because they’re not customer facing, they’re often hard to get time to work on or maintain — making them stagnate or have low quality; the functionality is often trivial (and potentially boring) to build; and it’s usually hard to get any team to own it. What also inevitably happens is that teams and non-technical stakeholders end up relying heavily on the internal tool and the lack of attention and quality end up biting you.

Ideally, you’d want to spend as little time as possible on internal apps, but have them be reliable, cheap to extend, and be welcoming/maintainable by a wide variety of teammates. Phoenix LiveView checks all of those boxes.

My team at work was recently in the situation of needing an internal app built a few weeks before the launch of an important product. The app would give us visibility into numerous backend services involved in the product and also provide monitoring capabilities for purchased goods. The problem was that all of the teams involved with the launch were swamped with bug fixing, last minute polish on features, and end-to-end testing. Similar to my earlier points, it would be hard to get this internal app prioritized before launch, given that it was only to be used internally. It also didn’t make sense for a disconnected team to build it for us since we’d need control over rolling out features as we learned more about our needs.

I volunteered to lead this effort and build the first version. To start, I aggregated a list of all of the features we’d need for the app and thought through the tradeoffs of various tech choices.

Technology choices

The standard approach at our company would be to spin up a single page React app and a Java JSON API backend. Traditionally, this would shake out to needing both a frontend (FE) and backend (BE) dev working on this tool. Even with a single fullstack dev, this would still be a lot of work.

This tool didn’t need a single page application: it didn’t have rich user interactions (would be mostly form submissions and some tables) and didn’t need offline capabilities. We had ideas of realtime features (like maps of ad campaign interactions) that could involve more client-side interactions, but those were nice-to-have and we still weren’t convinced an SPA was needed for that. Having to write all of the frontend code; wrestle with webpack configuration, react router, and react form; and also write all of the backend endpoints seemed daunting for little gain. It would have occupied all of my time and may not have launched ahead of our product launch.

We could have pursued a purely server-rendered approach, but our Java stack wasn’t built for this. Java would also (historically) limit contributions to backend devs. Using Node.js would have worked, except that we had other internal tools in Node that would fall over (affecting all requests) with a single unhandled exception. From experience, Node requires too much rigor, would have a high barrier to entry for those who weren’t aware of Node’s fragility (both in error handling and concurrency), and we didn’t want contributions isolated to frontend devs.

During my time experimenting with Elixir and the BEAM for high traffic backend services, I saw a lot of talk about Phoenix LiveView, but hadn’t used it before. It billed itself as a framework for web applications that didn’t require writing JavaScript, but would give you a single page app feel with bidirectional websocket communication, and provide all of the benefits of Elixir and the BEAM (terseness, fault-tolerance, cheap concurrency, and deep introspection). On paper, it seemed like it would solve for our constraints: limited peoplepower, a time crunch, and still produce a reliable and extensible product. Since the standard SPA + JSON API stack was always available as a fallback, a rewrite away from Elixir would be straightforward.

What’s also nice about internal apps is that you can put it behind a VPN or SSO requirement so it’s not accessible to the public. If there’s any apprehension to using non-standard technology for security concerns, using it internally is a good way to vet the tech.

We ended up choosing to try Phoenix LiveView as a proof of concept. Success was defined as: getting it into production quickly on company infrastructure, not actually having to write lots of frontend code, and getting contributions from others (particularly folks who had no Elixir experience).

So how did it go?

It took 1 day to get the first feature built locally — having no experience with LiveView, and 1 more day to get it into production (using docker and getting it working behind VPN). After that, new features (which took in some input, called a service, and printed the response on screen) took 1 or 2 minutes to implement. With tests (using Mock but later switching to Mox) taking 5 to 10 minutes to finish (getting mock data took the longest) writing.

Within a week or two, the tool started to get used daily. After a few weeks, we had a double digit number of features (with tests and no JavaScript written) built largely by me, with some contributions from others. It proved that with minimal resources, you could get a webapp shipped in less than half the time it would have taken 2 people to build. LiveView made building a relatively boring but impactful webapp educational, fast, fun, and engaging. We launched our big product and teams were immediately thankful for this internal tool. Today, it’s relied on heavily by technical and non-technical stakeholders.

We used Bulma as a CSS framework and it made layouts and styling super easy even for folks with no frontend experience. Tailwind would also work.

Surprisingly, Data Engineers (who use Scala day to day) contributed because it was so easy. They wanted to contribute because there were no hurdles in their way: Elixir as a language was approachable and the amount of code involved for a new feature was super small. And building a new feature was copy/pasting a LiveView, changing some markup and what to do on form submission, and that was mostly it.

All of the contributors to the LiveView app are backend or data engineers. This was one of the most surprising results of using LiveView.

I wrote a lot of documentation (which feels normal in the Elixir community) inside the code with a readme/architecture guide for adding new features. It also helped to make sure folks that wanted to contribute knew where to go for help. Some 1 on 1 overviews did wonders for avoiding that initial fear of using something new.

Early on, I ended up using LiveComponents (I was thinking too much like React) for all of the features. However, I later realized that using nested LiveViews was the better choice for isolating failures: that way if a particular feature/component had an error when contacting a backend service, only that part of the page would die and get automatically rerendered.

We had an SSO proxy (that only existed in production) in front of the webapp; the proxy passes some headers and we wanted to see what the values were but forgot to Logger.info them. So, instead, we used Rexbug in an iex shell on a production pod to trace/spy on methods that handle the headers — seeing the values and confirming that they were being processed properly in our application logic. This blew our minds. No need for adding temporary logs and redeploying. You can’t unsee this and will miss it when not using the BEAM.

Communicating with other internal backend services was pretty straightforward. It’s easy to speak HTTP or GRPC in Elixir-land. And if you have a custom backend communication protocol, you can shell out to a cli or proxy that speaks that protocol (assuming you can take the performance hit of shelling out; internal apps have low traffic, so it was fine for us) or write a client that implements your protocol. We tried to be pragmatic here: we didn’t want to reinvent business logic or client libraries that already existed in Java, so we limited the LiveView app to be the presentation layer and router to appropriate backend services. It fit that niche beautifully.

To this day (8 months later, as of this writing), zero JavaScript was written for this project. Zero. LiveView lived up to the hype.

In terms of reliability, the app has had no downtime and no major bugs (likely thanks to how little code we had to write to get features built). The self-healing nature of LiveViews made it so that transient errors were handled with retries built in.

Teams that don’t have frontend engineers see the value in a LiveView-like solution. They’re tired of maintaining their attempt at a React SPA. They’d much rather have a more backend-focused approach. They also don’t want to have to hire a frontend dev just for an internal tool.

What didn’t go so well?

If your company relies heavily on React and uses CSS-in-JS as a way to style components, that might bite you and your internal app won’t look the same as other web properties. That said, does your internal app need to look like your production apps? It’s solvable, but wasn’t worth the effort in our case.

We wanted to use phoenix’s form_for view helper and leverage its errors for form validation, but it seemed too coupled to changesets, which seemed coupled to Ecto at first glance (at least in all the docs and articles we looked at). Using raw html forms with put_flash errors rendered as a span worked well, except when you had more than one input field and wanted more control over where/when to show errors. We ended up moving to use Ecto schemaless changesets, but it’s intimidating to new contributors (comes off magical and introduces boilerplate). Compared to react-form, it’s much easier to navigate, but you need to be cautious of how much you’re asking new contributors to learn just to write a new feature.

If you have deep component/liveview hierarchies, Surface is the library to reach for. It was a bit too early for us months ago, but we can’t wait to revisit it to get that JSX-like readability for defining component hierarchies. Newcomers don’t seem to mind lots of live_render calls, despite the verbosity. We also have more independent, but nested liveviews, as opposed to components that are in charge of rendering data with no side effects. So I’m not yet sure we’ll get a ton of value aside from the increase in readability.

If you’re going to introduce LiveView (or really anything different/non-standard), you need at least one person to be the shepherd–to point folks in the right direction, answer questions, and resolve any missing integrations or infrastructure hiccups. This can be overwhelming at times, but you need to realize pushing technology boundaries is a marathon. You need to really believe in the value of the technology and be willing to stake your reputation on it if you’d like to see it more widely adopted.

What’s next

LiveView has been a big success in my opinion and the business value is clear to our team’s management. Internally, we’re starting to see LiveView as a tool for quickly building high quality dev tools and prototypes. Previously, spinning up a new app would seem like a large investment or require a large sacrifice in prioritization; now it’s “maybe we could do this quickly in LiveView.”

I’d urge you to try LiveView if any of our constraints apply to your apps, internal or otherwise. Cheers!

You can hear more about this story on this ThinkingElixir podcast episode.

--

--