When most people think of web apps, they think of progressive web apps (PWAs). The problem is that PWAs are essentially just locally cached web pages that generally still need to connect to a server to fully function. They aren’t complete apps in and of themselves.
But what if you could package the entire web stack — i.e., both client-side and server-side — as a standalone desktop application?
It might sound crazy, but it isn’t.
Keep reading to learn more about how we packaged a full Golang app, Focalboard, as a desktop application and shipped it via the Mac and Windows App Stores.
Why we built Focalboard as a standalone desktop application
We built Focalboard as an open source alternative to Trello, Notion and Asana to help users work as productively as they can.
To this end, we wanted to make it as easy as possible to download and use our app via popular app stores. We did this by bundling our server and web app in a desktop app package. To the end user, it downloads and works just like any standalone desktop app.
This approach also enables us to utilize the exact same server and web client code. We’re not porting to React Native or Electron. Instead, we write code once and test it once, which makes things more efficient for our developers.
This also enables us to keep the binary size small compared to Electron, which requires you to include Chrome and the relevant resources that come with it.
What the process looked like
We started our journey with a traditional multi-user Golang server that runs on Linux. This server includes a React and TypeScript web client packaged with Webpack. The exact same server and web client code compiles to all platforms. The only major difference is that data is stored in SQLite with the desktop app vs. Postgres on the server.
We also implemented a special “single-user” mode for the desktop app, which replaces the user registration and login but otherwise uses the same security framework.
Next, we wrote a thin native client application for each platform: a C# WPF app for Windows and a Swift app for macOS. These apps are just responsible for launching the server and opening a window with a native WebView to localhost. They also allow us to add platform-specific features so everything looks and behaves like a native app — as it should, because it is one.
What we learned about building standalone web apps
Along the way, we encountered some hiccups, so we’re passing along the learnings.
Our first attempt at this was to bundle the web server and client code with some simple bootstrap code that opened a browser window to localhost. This worked fine for development, but it quickly showed limitations: It didn’t feel like a native desktop app, and there were security vulnerabilities passing information via the Chrome developer interface.
So, we eventually bit the bullet and wrote native shells for each platform. This eventually worked well, but there were a few issues we ran into.
1. You need to bundle a WPF app as a UWP app on Windows
On Windows, one gotcha was choosing between building a WPF (Windows Presentation Framework) or UWP (Universal Windows Platform) app. We tried UWP first because it’s the newer framework, but it turns out that couldn’t work because of the much more stringent limitations it imposes.
In the end, we found that the best solution was to build a WPF app that is packaged as a UWP app in an MSIX file. Microsoft had shipped several tools (e.g., Desktop converter and MSIX converter) to do this, but much of it has been deprecated and wasn’t documented well. We had to infer the required properties in the AppxManifest.xml file to enable runFullTrust, as well as the correct methods to call to get the application support directory path (which is different when running as a UWP app).
The eventual code is quite minimal, but it took many iterations to get there.
2. You need to install the WebView2 binary on Windows
This will eventually be part of the OS, so this is a temporary measure. But, at the time of this writing, you need to do this in order to support the new Chromium-based Edge browser. We added special code to download and install WebView2 if it isn’t available.
3. macOS requires some additional tooling to support file downloads
WKWebView on macOS handles file downloads slightly differently. After some trial and error, we found we had to use data URLs and a special handler in Swift to make exports work correctly.
4. You need to consider desktop security
Desktop is a different beast than the web. While they have a much smaller attack surface, it’s important to consider scenarios where malicious code is able to execute locally in the user’s session.
For example, each of our native apps auto-generates a random security token with each launch, and uses a random port number. These defense-in-depth techniques help mitigate security vulnerabilities — even in the critical case of the host OS being breached.
5. You need to deal with desktop OS code-signing requirements
Each OS has unique code-signing requirements you need to consider. Windows, for example, will not install unsigned .msix packages (but there’s a manual workaround: Add-AppxPackage via Powershell). At the same time, macOS requires users to right-click to open unsigned .app packages. Both of these are solved by shipping directly in the Mac and Windows App Stores.
6. You need to deal with App Store requirements
Speaking of app stores, each has very specific packaging and signing steps. Make sure you follow them in order to get your apps listed.
For macOS, you might find it easier to build and upload via Xcode, at least the first time. Automating this requires using the command-line tools to build, sign, and upload your project. In particular, the bundled Golang binary needs to be code-signed as well.
Need some help? Use our project as reference
Take our word for it: Many of these requirements were hard to pin down.
Luckily, since we already went through them, you don’t have to start from scratch. Check out the complete build process we followed for Focalboard on GitHub.
Thoughts on the future
The approach above for packaging apps means that you can use the best tool for each part of the stack:
- Golang (or other high-performance compiled languages) for the backend
- Your favorite web client stack for the frontend
- Native desktop code for OS-specific interfaces
It also means that the same code can be delivered at scale, with native app stores supporting desktop app delivery and cloud platforms for servers. For apps that need to support both multi-user cloud as well as single-user apps, this is a very compelling way to go.
Yes, people have been predicting the end of native apps forever. But, this time, the pendulum may be making a final swing in favor of the web stack — at least until AR/VR interfaces take over.
Now that you’re familiar with the process behind what we did, check out the results of our efforts by downloading Focalboard as a Windows or Mac desktop app today.