UP | HOME

Three Years of Phoenix Liveview and Elixir

An innocent side project turns into three years of upgrades, maintenance…and profit.

Intro

There are few aspects to this story.

  1. Releasing a profitable web application in several years while starting out with zero frontend skills.1Yes, zero.
  2. The numerous engineering do-overs because of lack of knowledge. I’ve essentially rebuilt core aspects of the software several times.
  3. Minimizing a tech stack for solo endeavours and commentary on the evolution, stability, and utility of Elixir and Phoenix.

Timeline to Ramen Profitability

2008-2019 - A hobby service

Once upon a time there was a semi-useful hobby project that was handed to me to maintain. It consisted of several different services:

  • a 32-bit architecture search index written in C++
  • an api server written in Erlang with C nifs
  • and a single page frontend using jquery

Having no frontend experience, I didn’t dare touch the frontend. But over time the backend got more difficult to maintain2The search was super-finicky to keep running, mostly because it could barely be compiled and had random memory issues on more modern distros when it even could be compiled. Essentially, I had been keeping it in a 32-bit container for years., so I started porting them.

A rough timeline:

  • 2008-01 - Erlang, C++ search, and JQuery SPA
  • 2016-01 - 10 days to Elixir Plug, Redis, C++ search, SPA.
  • 2019-04 - 5 days to Elixir Maru, Redis3On a side note, it’s actually quite incredible what you can build with Redis. In this particular project, zrangebyscore was completely abused and a re-design reduced the storage from 3GB to 200MB, and SPA
  • 2019-11 - 30 days to Phoenix Liveview4Before Liveview, I was using Drab. and Postgresql

The final rewrite was the longest, but it layed the foundation for actually building a web application as I learnt CSS and wrote my first frontend code ever.5Never touched frontend in 20 years. Protip for backend engineers - CSS and SQL are equally important languages.

2020-08 - Part-time - 8 months to an MVP

It was horrific looking. It was embarassing. It barely worked. And I had to be cajoled into releasing it and letting people use it.

But the slow grind of a few hours a day paid off and in a few months it was in the hands of users, which meant I could watch how they were using the software.

And in another few months, with some help of someone well-versed in design, a completely new UI was built.6It’s amusing how I used to think that backend engineering was hard until I watched in horror as people overwrote their data because of my ill-designed form-entry.

2021-08 - Full-time - 1 year to billing

After deciding to take a few months off, I wanted to work on this project full-time. I figured a year would be sufficient to bring it to closure. I was wrong. 7I’m at 1.5 years right now.

Quite a bit of this can be attributed to not knowing anything.

Here is a non-exhaustive list of items:

  1. Logins. Re-written three times. I was going for elegant and just sent an email with a login link. But after using it on mobile, I realized there was a problem in opening always on default browser.8Some software also pre-fetched the link, so when the user clicked on it, it was already expired.. And so I re-wrote it to send a one-time code, which should have been the end of it9Of course, it wasn’t.. But the login flow was confusing to users and they seemed to like passwords, so I had to put in passwords.
  2. Locations and autocomplete. Re-written three times. The initial implementation was pretty janky actually because I used the Phoenix Liveview example with datalist.10This is a terrible idea because of how browsers implement it, but I didn’t know enough CSS to do any better. Learn CSS. Then there was a problem with returning responses in sub-10ms range over 100k cities. I finally spent 100 hours in a week adding millions of locations and tuning the queries.11I swear that a ton of brilliant engineering happens in these insane concentrated states. I’m not sure how I did it, but some people who specialized in search thought it is so good it could be a standalone product.
  3. Settings. I started off storing settings in cookies with nothing server-side. After adding configuration options, due to the Phoenix session handling catching me by surprise, the cookie space was actually double what I thought, which meant I had to migrate. The new settings used a guid in the cookie, stored everything in ETS, synced the ETS prefs to DB, and did a distributed cache update on ETS for multiple machines if a pref was changed.12Quite frankly, the distributed cache update was the easy part. And the third iteration happened because I wanted to have more custom settings per object with global and local scope.
  4. Frontend. I’ve gone through several iterations of my frontend knowledge. I started off with CSS Grid to make a desktop and mobile view. To do the re-design, I ported the CSS to SASS13I didn’t even know any of the options at the time, but it did nesting.. It now worked well on five different screen sizes. At some point, I fell for the hype and tried Alpine. I already had solved the problem earlier with the checkbox hack, so it was a waste of several weeks. Currently, there is a total of 285 lines of javascript code in the project and 930 lines of SASS. I still have to write a PWA, so there is some more to learn.
  5. Domain expertise. At least the same amount of time was spent learning about the domain as all of the above.

Also, you might be wondering, “Why did this idiot build so much core functionality themselves? For instance, why not use Okta14Trauma. Have you had an enterprise deal with them? No, I don’t want to talk. for auth? Or Google or some other service for locations?

Simply put, I like privacy, performance, and stability.15And I felt like it. I even spent a few days designing my own logging service. I absolutely do not recommend that anyone do what I did.

2022-02 - 6 months to ramen profitability

The original user of the software was myself, so this was not really the goal. One thing led to another. Regardless, ramen profitability is one thing, but making up for opportunity cost is another.16Although, one should also account for their soul slowly being consumed in Silicon Valley.

There should probably be some lessons about the non-engineering side of things, but truth be told, I didn’t do anything. Regarding pricing, I knew that I would make bad decisions, so I let someone wiser than me set the pricing. And marketing has all been word of mouth.

And after all that, I’m actually a bit shocked at the line counts as about 20k is domain specific. 17cloc –force-lang=EEx,heex –categorized=/tmp/c1 ./lib/ src/ assets/css/*.scss assets/js/app.js

Language Files Blank Comment Code
Elixir 139 3785 7472 20147
EEx 45 513 0 5334
Sass 2 67 14 930
Markdown 3 211 0 581
C 4 105 209 491
JavaScript 1 38 79 285

The Minimal Solo Tech Stack

I’m a tiny bit anti-cloud at this stage of my life,18After spending a decade and several hundred million dollars, I really don’t want to look at AWS billing ever again. Or be taking out to a celebration dinner for giving them a 8 figures of money. so there are a minimal amount of services and tools.

More details are in each section, but my complete technology and tool list:

  • Language: Elixir
  • Framework: Phoenix Liveview
  • UX: SCSS via Dart Sass
  • Data: Postgresql and Sqlite
  • Infrastructure: Ansible, Runit, and Haproxy19I’m removing haproxy and going back to nginx. It irritates me.
  • Tools: Fossil for source code and Emacs for everything else.
  • Services: Paddle for billing, AWS SES for email, LetsEncrypt for certs, and Plausible for analytics.

Language - Elixir

I generally look for a few things in a language:

  • longevity, stability, and maintainability
  • ability to represent complex ideas with clarity
  • libraries and general ecosystem
  • performance and reliability

Longevity, Stability and Maintainability

Erlang has been around for roughly 30 years and it’s not going anywhere. Elixir, which is built on top of the BEAM VM20For years, I kept my usage of BEAM for backend systems relatively secret. of Erlang, is likely even more popular than Erlang at this point.

However, longevity is somewhat of an illusion since the only thing that matters is if your application can still run21In one job, we kept two application servers running WebLogic mapped to fixed ips for licensing running in 32-bit vms / containers for roughly a decade without any modifications. And yes, they were critical to the business. and be updated. Eventually, there is some reason that is required for an upgrade whether it be a platform change, security, or the language version has reached end of life.

From my experience, Erlang and Elixir, I believe, land in the middle of the pack on this unofficial ranking based on my official gauge of my irritation level with a language.

The top three:22Perhaps there are other languages that are better, but I just don’t have the experience with them.

  • C - It’s C. It’s irritating in different ways, but not this.
  • Vanilla Javascript/Jquery. Kind of impressed. I literally still have a single page of frontend code written in jquery by someone that is running fine after 11 years.
  • Erlang/Elixir - I was annoyed exactly once. BEAM OTP only supports the last release and there is roughly a new release every year. Elixir supports several versions of OTP. The single time I was irritated was early in the life of Elixir which resulted in a half-day of yak-shaving.23I wanted to upgrade an OS. However, I couldn’t install the ancient OTP I had available which then involved a chain reaction of needing to update Elixir followed by having to deal with dependency upgrades and having to re-write a bunch of code. Nowadays, Elixir is approaching the stability of Erlang libraries which just work and are rarely updated.

Then there some others which I rank not in my top 3.

  • PHP - Not very irritated. Of course, I used it back in the early 2000’s, so perhaps my memories are more fond.
  • Ruby - The 1.9.3 to 2.0 transition was pretty decent. I know this cause I barely remember it. I have gone through dependency hell a few times with some software written in Ruby, so there’s that.
  • Java - Mild irritation. Luckily, having an army of developers to deal with upgrades lessens my personal annoyance.
  • Python - The 2 to 3 transition was not good.
  • Anything else involving JS. Heaven help us all. You’re never sure if anything will work.

Ability to represent complex ideas

For systems and prototyping, it is tough to beat Erlang/Elixir.24Actually, I can’t think of a single language that does better. Lisp might be up there in different domains.

Out of the box, you get:

  • genserver - a representation of a server
  • a communication fabric - to communicate between different processes
  • supervision tree - to restart a server or process if it crashes
  • fault detection - to inform other processes
  • internal api versioning
  • a cache

With these, it is easy to model and represent systems.25Deployment is not linked to design, you can scale everything independently if needed later.

Let’s say you wanted to build a product that needs 3-4 services. Well, you can literally model that on a single node. This would involve writing several different Genservers, starting them under the supervision tree, and then allowing them to communicate via message passing.26You’d need Kubernetes, something like GRPC, Redis, and who knows what else to emulate this setup.

If you’re an architect that needs to figure out if a design is going to work, look no further than Elixir.27Just be aware that your code may run better than teams who spent months and you may feel sad and depressed about the world.

Library and Ecosystem

Elixir has evolved a great deal since I started in 2014. I could never actually recommend teams to use Erlang because of the lack of ecosystem, but that is no longer true for Elixir. I could go into more details, but a high-level overview:

  • standard library and documentation are very solid and evolved over time to the point dependencies could be removed.28And mix hex.docs offline is bliss after you block fonts.google.com.
  • mix is one of the best package managers and hands-down favorite from all the languages I’ve used.29Heard great things about Cargo, but never used Rust.
  • build and runtime configuration for deployment has improved to work out of the box with `mix release`.30I use distillery and still need to migrate.
  • ETS - the Erlang Term Store which serves as a cache
  • Hex PM has many packages, however, there is still no official Stripe, Paypal, or Paddle libraries. If you would like to use an esoteric db, good luck with that. On the positive side, the libraries that are available are good quality and the entire erlang ecosystem is available.
  • BEAM VM - the underlying virtual machine is extremely stable yet continues to improve.31A recent release added JIT which improved performance considerably on some applications.

Performance and Reliability

The BEAM VM has made many choices which allow systems to scale with some effort. Much has been written about the performance and reliability.

Some caveats, the BEAM VM is not great for computationally intensive tasks. If you’re looking for that, pick another language to write that code.32I have a few NIFs in C for that reason.

Framework - Phoenix Liveview

Everything I wrote about Elixir is important for backend systems. But how does anyone who only knows backend systems end up building a decent UI?33I actually don’t like frameworks, but Phoenix is so modular, it’s tough to dislike it. Well, here’s my experience with Phoenix Liveview since it was released.

Zero frontend experience

I started out with zero frontend experience. With backend systems, I actually never used a framework either.34Also, with great irony in the last three years, I’ve realized the principles that allow for maintainable and scalable backend architecture are the same for having good ux architecture for settings/components/etc.

So over many years, I experimented with Ember, Angular, Elm, Mithril, React/Redux, etc. I would usually spend 1-2 days every year which meant there was a brand-new thing to look at by the time I got back to it.35My favorite was spending a few days writing an Angular application to find out Angular 2 was coming out and wasn’t backward compatible. Even the build tools are out of control.36Bundler, webpack, yarn, and npm and I’m still wondering why I couldn’t <script src=bla.js> and be done with it.

With LiveView, a total of 285 lines of javascript code and 930 lines of SASS was written in the past three years.37I wasted three weeks experimenting with AlpineJS and then went back and wrote 50 lines of hooks. I’ve spent the majority of my time on the business domain.

So, in my opinion, if someone can figure out the insanity of the Javascript frontend ecosystem, they can easily learn Elixir and Phoenix Liveview.

Maintainability

I’ve been using Liveview from 0.1 to 0.17.7. There have been a few breaking changes between versions and I believe I’ve spent roughly 10 days going through liveview upgrades in 3 years.38It took 5 days to update from 0.15.4 to 0.17.7 where 3000 lines of template got updated. With various other changes, the socket data was reduced by 50k uncompressed.

There are 18,000 lines of domain specific Elixir-code and 3000 lines of templates.

Code quality is always an amusing discussion since I’ve never been a fan of test coverage.39I believe in fear-based testing. If I’m ever scared of changing something, I write tests around it. There are roughly 5000 lines of tests and probably another 5000 lines of doctests, but absolutely zero coverage of LiveView.

The application consists of different stateful components which I manually test after changes. Once a component is created, there is just a question of how it is populated. After the first year, I re-factored many things to use structs instead of maps.40Yes, they are the same thing, but you know what you’re getting with a struct. In the second year, I discovered protocols which simplified several designs of new features.

Regarding static analysis, I tried using dialyxir and added specs to many functions, however, this was not very useful. I did want to try static typing, but I’m not sure how much it would really help with my design and code organization. Essentially, if anything is wrong, the application will crash immediately. It’s rather hard to miss. At least for this application, I don’t believe static types would have been useful.41They seem to be hot right now, so I do want to try them. Usually, my modules/genservers/services are tiny. For instance, the location code is just 500 lines. I’m just confused where static typing would be useful.

In lieu of static typing, I do depend on doc tests.

Functional codebases are generally easier to maintain and since I wrote all the code, I can’t say I’m having any issues.

User Performance Perception

Users are worldwide while the server is on the east-coast of the US. LiveView is much faster than the SPA that I had before and much more usable on mobile and high-latency connections. The reasons are:

  • Liveview generates a full page that is useful on the first render. The SPA would load a skeleton page and then start populating the page. The first render is roughly 25k gzipped and requires no javascript, so on low-end cpu and slow connections it is magnitudes faster.
  • No CDN used and only requests to a single domain. The SPA would require various javascript libraries which I had pulled from a CDN. When travelling in India, I found that DNS requests to the CDN would take seconds sometimes.42Amusingly, the location service I built for fun gives a noticable performance boost on low-end devices. Making multiple tcp connections and ssl negotiations also adds to latency.
  • The full page load contains everything the user needs after two requests which are the main page (25k gzipped) and the app.css(10k gzip). The javascript loads afterwards and establishes the websocket(80k gzip)43I added a tour at some point, but looking at the size really makes me irritated enough to remove it..
  • Most users explore the page via various tabs which are css-based.44I think this is called optimistic ui? There is no javascript involved in the use of the page even though there appear to be dynamic elements. Going to the backend only happens when the user wants something re-rendered.
  • The websocket is established after roughly 1-2s if the js is cached. A full page change of all components over the socket is 70k of raw json which is 8k gzipped45Note this is 3x smaller than the original page render.. A full-page re-render of all components takes roughly 400-600ms from India connecting to a US server. On the east coast of the US, a re-render takes less than 100ms.
  • If the websocket is broken for whatever reason, there is a phx-error class which becomes active. This allows the user to know that they are no longer connected to the server. Once re-connected, the notice goes away. In practice, this doesn’t interfere with the usage due to the fact that everything the user needs has already been loaded, but hidden with css.
  • Websocket reconnections call a form-recovery to ensure that the user state can be re-populated on the server side.

Since this application would never be able to do everything client-side anyway46But maybe with WASM eventually., this architecture is as fast as you can get at the expense of cpu and memory.

Server performance - CPU and Memory usage

LiveView keeps track of the user state on the server side and then sends a diff to the client to render. While the connection is active, it uses roughly 3MB of memory on the server side. After 15 seconds, the connection is hibernated and reduces to 150kB.

Theoretical upper estimate: The memory usage depends on the number of concurrent active connections. I don’t imagine this application will ever have 10,000 concurrent users, but in that case the server would need roughly 30GB of ram. From the compute side, estimating at an update every 10 seconds requires 1000qps. The worst page takes 100ms which would imply that 100 cores are needed. In reality, the majority of page events take less than 20ms which would suggest roughly 20 cores.47This is roughly $200 / month at Hetzner.

Real-world estimate: From real-world usage, roughly 1GB of ram per 200 users is required for this application and at most 5-10 are active at a time. So if there was 10,000 concurrent users, this could still run on a single server and give mega-monies.

Given all that, I’m good with the server-side performance.48For now. Optimization is fun.

Tradeoffs

Users have asked for an offline mode. I’m not sure if this is feasible yet, but the application has been designed to be cached. There is an open question if the application can connect properly to the websocket without an updated csrf token.

Summary

For my application, Phoenix LiveView is really, really good. The cognitive overhead is much lower and I can’t say enough good things about it. Simply put, it made a herculean task of building web applications, merely difficult. That is true innovation.49Fire And Motion – Joel on Software covers my thoughts pretty well on this. While everyone else is busy keeping up with the tech and wasting their time, liveview just works.

UX - CSS-Grid and SASS

The rest of the frontend50I never quite understood what CSS frameworks did. Phoenix comes by default with Milligram which seemed fine. I explored various frameworks like Bootstrap, Bulma, etc. and just like with javascript I gave up. is based on:

  • CSS Grid
  • SASS
  • Esbuild

The major problem with frontend is it’s quite difficult to know where to start. So for the newbies, please just go learn CSS Grid. After learning it from first principles, I never needed to search for how to align things again.51It took a day. Incredibly good ROI.

At some point, after a UI redesign, I liked the idea of having nesting inside CSS52I’m not actually sure if SASS is even required, but I didn’t know CSS well enough at the time. At some point, I may consider re-writing the SCSS to just be plain CSS, but this is hardly a pain point. and I saw that SASS allowed that. Considering that it had been around for 15 years, I figured it was safer than PostCSS.

Earlier versions of Phoenix used Webpack and configuring Sass plugins and correct version dependencies required various gymnastics53And prayers and sacrifices..

And then a miracle happened and Phoenix switched to esbuild by default and a dart_sass library became available. Now there is no dependencies on npm and node at all.54I actually like javascript as a language because of it’s longevity and portability, but whatever is going on with this ecosystem is a bit of an abomination.

Data - Postgresql and Sqlite

This storage layer is given by:

  • Postgresql - relational database used for dynamic data.
  • Sqlite - used for read-only data.

And the following come included with Elixir:

  • Erlang Term Storage - ETS is used for caching.
  • Ecto - a composable ORM which doesn’t drive me crazy.55Unlike hibernate.

Very early versions used Redis as a read-only store, but after changing ideas, it was dropped for Postgresql and ETS replaced it as a cache.

Infrastructure - Ansible, Runit, and Haproxy

The infrastructure layer consists of a server for production(4GB) and staging(1GB).

The main tools:

  • Ansible - a very simple deployment tool.
  • Runit - for supervising the single binary.56I supposed I could have just used systemd, but I’ve been using daemontools/runit for 20 years now.
  • Haproxy - SSL termination.57I don’t know why I used haproxy instead of using nginx. I thought it would be better for rate limiting by ip address, but I’m considering just doing that in Elixir instead now.

Other:

  • Logging - Using vector to transport logs and been experimenting with my own viewing service.
  • Guix - Guix is fully free Linux distribution inspired by Nix.58Unlike Nix, the tooling actually makes sense. Still experimenting with it at this stage.

Eventually, I plan to move to Fly.io which have drops across the world and supports anycast.59Though rolling my own anycast network seems interesting. Once the latency is under 10ms, I would expect the performance to be desktop like.

Tools - Emacs and Fossil

  • Emacs - My setup is at The Way of Emacs. For a dev workflow, my notes are in a single org-file which has 5000 lines and 38k words. I don’t use a ticket system, but occasionally I do use the fossil wiki to keep notes on important branches.
  • Fossil - After using Git for over a decade, I switched. It’s been more productive than git for me. It’s fantastic. 60And it’s so easy to use, I didn’t even lose any data while learning. On a different note, I wonder if git has lost more data than rm -rf?

Services - AWS SES, Paddle, and Plausible

And finally, the services I use.

  • Paddle - SAAS billing. I considered Stripe and Paypal, but Paddle was the only one that dealt with taxes at the time.
  • Plausible - Analytics.61Normally, I wouldn’t care enough to use analytics, but I lifted the Paddle implementation from them, so I figured I can buy a subscription.
  • AWS SES - sending emails.
  • Letsencrypt - SSL certificates.

Conclusion

The project is ramen profitable after 1.5 years of full-time work.62And numerous years part-time. And several month-long breaks.

For a solo founder, time is extremely valuable63And shouldn’t be spending hours writing 5000 word articles. and the best stack is the one they are familiar with.

Having already worked with Elixir, continuing with Phoenix Liveview allowed me to minimize my time on technology and focus on the product. I had to learn frontend from scratch, but the majority of the time was spent learning the domain and iterating over technical designs64Sometimes silly things, but still better than messing around with tech..

For those evaluating technologies, this project was built over roughly 3 years, consists of 20k lines of elixir, 5k lines of templates, 1k lines of scss, and 300 lines of javascript.

The cost is roughly $30/month across all enviroments. And if the project does become immensely successful, my calculations indicate it can scale to 10k concurrent users65That means there’s about 1M users using the service and I’m not sure that many exist. on a 32 GB box with the current design.

If you’re building a complex web application66If you’re doing simple things, there’s something to be said about php. Elixir and Phoenix Liveview might be the most minimal and future-proof tech stack.67There are some similar frameworks such as LiveWire, Hotwire, etc. which may also be in the running.

My complete technology and tool list:

  • Language: Elixir
  • Framework: Phoenix Liveview
  • UX: SCSS via Dart Sass
  • Data: Postgresql and Sqlite
  • Infrastructure: Ansible, Runit, and Haproxy68I’m removing haproxy and going back to nginx. It irritates me.
  • Tools: Fossil for source code and Emacs for everything else.
  • Services: Paddle for billing, AWS SES for email, LetsEncrypt for certs, and Plausible for analytics.

Date: 2022-03-16 23:30