Building Apps with Tauri and Elixir

October 19, 2023

For the longest time, building desktop apps was a daunting task to web developers. That is, until technologies like Electron made creating these apps more approachable to a wider audience. Today, we’ve got a wide array of native applications built with solutions like Electron, Tauri, Capacitor, and many more. While these are great solutions, sometimes configuration can be tricky and the applications we create can become somewhat bloated in terms of memory usage.

The Elixir programming language is no stranger to desktop applications as the language actually supports building them out of the box. It uses wxWidgets: a C++ library that lets developers create applications for Windows, macOS, Linux and other platforms with a single code base. But wxWidgets has a very complex API, and doesn’t solve issues that usually come with desktop applications around packaging.

Thus, we set out to build a desktop application using a LiveView from the Phoenix Framework in Elixir. For the uninitiated, a LiveView is a process that receives events, updates its state, and renders updates to a page as diffs. The LiveView programming model is declarative: instead of saying “once event X happens, change Y on the page”, events in LiveView are regular messages which may cause changes to its state.

The end goal we were working towards was:

  1. An easy development cycle.
  2. A lightweight and snappy application.
  3. First-class bundling support.

Enter Tauri

From the moment we discovered Tauri, we really felt like this was the perfect fit. The API is really solid, the configuration files are minimal and easy to understand, and the usage of Rust makes it way easier to add new functionalities and think about interesting ways of interoperating with Elixir via the Rustler library.

The biggest benefits we derived from Tauri were Wry and the sidecar mechanism. Wry (the second half of Tauri: tao/wry) is a cross-platform WebView rendering library in Rust that supports all major desktop platforms like Windows, macOS, and Linux. It essentially spins up a native web view from whatever operating system it’s running on and doesn’t require an application to bundle one with it. Wry greatly reduces the overhead of “pushing” a browser to our users, instead leaning on the host OS to handle rendering a web view. This made our applications really lean.

With Tauri apps, you may need to bundle dependent binaries with your application to make your application work or prevent users from installing additional dependencies (e.g., Node.js or Python). We call these dependent binaries sidecars. Sidecars are a beautiful solution that enabled us to fix quite a few issues regarding the lifecycle of complex applications since it allowed us to bundle multiple executables into our solution.

Due to sidecars, we are able to have a Rust application that handles the window and the communication with the OS and a separate Elixir application that handles the business logic and the communication with the Rust application via Tauri’s sidecar mechanism.

Setting up Tauri for Elixir Phoenix

Since we’re using a Phoenix project, we already have a full folder structure to use which means that we had to find a proper way to install Tauri and use it in a project which is trickier than it seems since we want to install it scoped to our project only.

Fortunately the Elixir community already had a similar issue with Tailwind so we used the same methodology where we download the required dependencies into our Phoenix _build folder and run from there.

Knowing all of this, we automated the full setup by doing the following steps:

  • Installing tauri-cli using cargo into the _build folder
  • Run tauri init with multiple options setup
    • --app-name set to the application name
    • --window-title set to a window name
    • --dev-path and --dist-dir with the host and port to be used by the web view
    • --directory and --tauri-path to set our tauri source directory
  • Writing our own cargo.toml file
  • Set the expected application name
  • Change dependencies to use crates.io instead of folders such is the default behavior of tauri init
  • Writing our main.rs to write the code required to:
  • Start the sidecar
  • Check if sidecar is connected
  • Start Tauri
  • Change the Tauri config files to
  • Setup Sidecar external binary
  • Setup allowList required to run our sidecar

This was essentially the work required to add Elixir interop to Tauri projects. It required a small set of changes and we were able to use the full power of the Tauri CLI and a Tauri project that we could completely change at will.

Now came one of the biggest questions: How can we properly sidecar a Phoenix application?

The answer was given by the Elixir community with burrito which enables users to pack up everything an Elixir application needs within a binary namely Zig Archiver to package the binary and Zig Wrapper that wraps the Erlang Virtual Machine to be used in multiple platforms (Zig + Rust in the same project 🤯).

With burrito, a user just needs to run two commands to fully build a desktop application using Elixir and Tauri:

  • mix ex_tauri.install which installs tauri-cli only for this project and creates all the files mentioned above
  • mix ex_tauri dev: which passes through the commands to our installed Tauri process

And to bundle it up you would just need mix ex_tauri build and it’s done!

This was all in a new library that is still work in progress but very promising. We are eager to get contributions from the community. Learn more at ex_tauri on GitHub.

The Tradeoffs

Currently the fact that we weren’t able to use the binary directly means that we need to install the tauri-cli using cargo which is non-optimal and increases time to first screen by a lot. Additionally the fact that some options are trickier or not available to set via CLI forces us to rewrite some files, for example we can’t set sidecar information.

Another tradeoff is the need to have a communication line between Rust and Elixir, that is still largely undecided but we might look into Rustler as a potential route or use a socket (similar to Docker).

Finally, there’s a tricky element in the need to fully compile and package Elixir all the time, even in dev mode. However, this also feels like a solvable problem with a bit more time and effort.

Conclusion

Truly Tauri offers what, in our opinion, is the strongest case at the moment to build desktop applications independent of the use case. The fact that with a couple of days of work and some research and help from the Tauri team there’s a library to build desktop applications that can be bundled up securely with just a couple of commands shows how versatile Tauri is and how approachable it is to build native applications regardless of the stack.

We’ve used Elixir with the Phoenix Framework because it’s our preferred stack, but the methodology we used could be used with any language and with relatively low effort. We can’t wait to work more with Tauri since we’re still just scratching the surface of what this project is capable of.