I Do Love to Really Travel

Kris Gage wrote a very interesting piece on her dislike for travel, which is a huge self-awareness and self-improvement topic that has been the centerpiece of most of my adult life (yeah, I left Kent…

Smartphone

独家优惠奖金 100% 高达 1 BTC + 180 免费旋转




How we built an app update flow our users love

This is how updating the app looks:

Our custom updating experience helps us to move the majority of our users to a new version within 24 hours. On top, our users praise the experience:

Let’s dive into why and how we built this crucial piece of infrastructure.

Raycast is different to other desktop apps: It’s ephemeral and you open it to have quick access to your tools. If you don’t need it, it stays out of your way — the app doesn’t have a dock icon, neither does it show up in your app switcher. It’s designed to keep you focused.

We release a new version every week. With such a short release cycle, our users update the app constantly. Having a window popping up to prompt for an update just when you wanted to quickly check something doesn’t feel right. We wanted our updating experience align with our product philosophies: Simple, fast and delightful.

Finding time to change fundamental infrastructure is challenging for a small startup. There is constantly something else popping up that seems more urgent. What’s easy to forget, is that infrastructure will pay off in the long run. In the case of our updater, it’s something that most users do once a week.

In August, we improved the foundation and iterated on the design of the app. This was the time when we also decided to build our own app updater. The first step was to define requirements: We wanted to make sure that the update flow integrates into the user experience, it’s easy to see What’s New after an update and support internal releases for our nightly builds, which we use to dogfood new features. In short, it should be fun to update the app!

With the requirements defined, we looked into existing frameworks for app updates:

Eventually, we decided against forking any of the mentioned frameworks. We took inspirations from each of them and came up with the following solution:

Let’s walk through a new app update step by step.

After a successful build, the CI drafts a new release with the DMG attached. The oncall fills out the prepared release notes and publishes the release. Then it’s available to download.

The app checks for updates at least once a day. For this, it makes a networking request to our own server with the current app version as a query parameter. The server is a simple middleware that abstracts away the GitHub Releases API. This is helpful in the case we want to switch to another storage system for the binaries. The endpoint takes the current app version, fetches the latest release from GitHub and compares the semantic version numbers. If the client is up-to-date, it returns a 204 HTTP status code. If the client needs an update, it responds with 200 and some metadata about the update:

We use NSBackgroundActivityScheduler and URLSession for automatic checks. The scheduler isn’t 100% reliable. In our case, we want to make sure that the client checks at least once a day for an update. To guarantee this, we check the last time we requested an app update with Calendar.current.isDateInToday(lastAppUpdateCheckDate).

In addition to the automatic checks, we have a Check For Updates command and a status menu item to get the latest release.

To make the installation frictionless, the client starts downloading the DMG in the background without asking the user. We can do this because our app is small (~10 MB at the time of this post) and our users are usually on WiFi. After successful downloading, we mount the disk image with the following script:

Next, we move the .app file from the mounted DMG to the Application Support directory and compare the bundle identifiers. If the bundle identifiers are the same, we compare the code signatures. We use Apple’s Security framework for this:

What the above code does, is pretty much the same as the CLI tool codesign. We then simply compare the extracted code signature of the new update with the one of the existing app. If everything matches, we have an update to install 🎉

Last step, be a good citizen and detach the disk image to clean up after ourselves:

After successful security checks, the available app update is presented on the top of the root search. You just need to hit
to install the update ✨

New update is available to install

First, we use the xattr command line tool to remove the quarantine flag. This avoids a system alert asking the user to run this program because it was downloaded from the internet. We can do this because we know it’s our own verified binary that we execute.

Next, we simply swap the current app with the update. Thanks to Unix, we can just move the currently running app to the Trash.

Last but not least, we terminate the old version of the app which is now located in the Trash. The above script makes sure that the update opens and that’s it. We’re on a new version of Raycast 🏁

When the update launches, we interpret the previously mentioned launch argument. This way we can detect if it’s a new version and show the release notes. The notes are fetched via our middleware from the GitHub Releases API.

Release notes surfaced after updating the app

We had a working app updating flow and nobody asked us to improve it. So was it worth all the effort?! The short answer is yes. The new flow is much smoother and users sent us positive feedback about it. It also allowed us to dogfood our own features quicker. The team can easily install the daily internal releases. Building foundational pieces as a startup with not many resources sounds scary. If you are opinionated about your solution and optimize for your specific use case, complex problems become much simpler and suddenly are easy to implement.

Now we own the code, understand what it does and can extend it. We recently added automatic background updates to make sure that our users are on the latest version of Raycast. This was easy to add and we tailored the solution again to our specific setup. We have more ideas like differentiate hotfixes and minor/major releases, stage the rollout of updates or introduce another update channel for alpha testers. All of this is now much easier when we own the code end to end.

If you want to build something high quality and outstanding, you have to go the extra mile and take the risk to tackle hard problems. In our case it paid off and I’m sure it will do for you as well.

Add a comment

Related posts:

10 Ways to Increase Your Sales Productivity

Sales force productivity is one of the most important elements for a successful business. Whatever it’s about — products or services — sales are always involved. Skillfully done, they increase income…