Launching Livebook using LiveView

Fly.io runs apps close to users, by transmuting Docker containers into micro-VMs that run on our own hardware around the world. This is a post about writing our LiveView launcher that launches a Livebook instance for you on Fly.io. If you're more interested in deploying your own project, the easiest way to learn more is to try it out; you can be up and running in just a couple minutes.

The Livebook launcher was super fun to create. Everything happens in a single LiveView process. There was a problem though… it wasn't great if we were in the middle of deploying the app for the user and one these things happened:

  • User hits "refresh" on their browser
  • Server is deployed or restarted

The Problem

The problem was there are generated secrets like liveview_password. Once we send that off to have our Livebook deployed, we could never get those values back! They are stored in Vault and even we are protected from being able to access secrets stored there. After all, in cases like this, they aren't our secrets!

This presented a UX problem. We had private data that was only temporarily stored in a LiveView process. If anything went wrong with the user's connection or the server was restarted, the UI would lose the secrets forever! Part of the UI design is to show all the information to the user at the end of the process.

Livebook launcher deployment summary

How could we do that in a situation like this? I considered using a database or a Redis server to store that state. I didn't like it though because we're trying not to keep that data around on our servers.

The Solution

The solution was to encrypt the private data we want to keep using Phoenix.Token.encrypt/4. This not only signs the data to prevent tampering but it also encrypts the contents keeping them private. Then we could store that encrypted text blob in the browser's SessionStorage. That way is stays under the users's control. This also means they are available to be pushed back up to the server!

LiveView has some powerful features. One is called hooks and they let the server and client communicate in both directions. I defined some simple hooks in a Javascript file like this.

export const hooks = {
  mounted() {
    this.handleEvent("store", (obj) => this.store(obj))
    this.handleEvent("clear", (obj) => this.clear(obj))
    this.handleEvent("restore", (obj) => this.restore(obj))
  },

  store(obj) {
    sessionStorage.setItem(obj.key, obj.data)
  },

  restore(obj) {
    var data = sessionStorage.getItem(obj.key)
    this.pushEvent("restoreSettings", data)
  },

  clear(obj) {
    sessionStorage.removeItem(obj.key)
  }
}

If a user reloads the page, the server sends a push_event command over the websocket to the browser asking it to "restore" any settings it had under the "livebook" key in the SessionStorage. If the client has something to send, the server gets back the data, decrypts it using Phoenix.Token.decrypt/4, and resumes tracking the user's deployment progress.

Likewise, if the server is restarted when we deploy a new version, the client provides the private data and after a little UI catch up, it continues tracking the progress of the deploy.

That's pretty much all the javascript I had to write do deliver this feature!

After the deployment succeeds or fails, we "clear" the cached data from the browser's SessionStorage just to keep things tidy. Also, when the user closes their browser tab everything in their SessionStorage is cleared out as well.

Hope that sheds some light on what's happening and gives you some ideas for solving similar problems!

Launch your Livebook now

Deploy your Livebook instance now. Hit refresh during the deployment process and see it recover.

Launch yours now