Writing completely offline web apps
Tags: javascript, webdev, html
Published: 2022-10-19 15:23:42

It seems contradictory in nature to make a web application that runs entirely offline.  That is, it only needs to be loaded one time and then never makes another web request again.  This is different from online apps that can lose Internet connectivity for a short while but sync when Internet functionality is restored.

Building a fully offline application is a tad bit more difficult to do than it seems on the surface.  A fully offline web application must:

- Load all resources in advance (HTML, Javascript, CSS, and images).
- Handle its own navigation non-recursively and never navigate to another page.
- Validate inputs in the web browser client.
- Handle its own data storage and retrieval.
- Not attempt to make requests outside of the initial app load (i.e. no form submissions or API calls).
- Try to maybe have some security theater.

The need to build a fully offline application is pretty rare.  An example is [Offline Forms](https://github.com/cubiclesoft/offline-forms), which can design forms and gather data of a non-personal/non-financial nature even when Internet connectivity is spotty or non-existent:

{% embed https://github.com/cubiclesoft/offline-forms %}

Offline Forms is a quick-n-dirty app I threw together just to test the idea of building entirely offline applications.  It works fine and forms can be designed in a matter of minutes with the point-and-click interface, but don't expect to be impressed.  I ended up spending most of the time fixing a number of minor bugs in [JS FlexForms](https://github.com/cubiclesoft/js-flexforms).

## Security Theater

The biggest downside to building an entirely offline app is security.  The web browser itself isn't secure and neither is any data stored in it that Javascript has access to (cookies, Local Storage, Session Storage, etc).  A relatively simple bookmarklet can exfiltrate data from the browser to any properly configured remote host.  The only security in Offline Forms is a repeatedly hashed password but that's just a minor deterrence.

Offline Forms allows custom Javascript to be written, executed, and even exported/imported (mostly intended for validation purposes).  The importer displays a warning when importing a form with custom Javascript but not everyone reads warnings or pays attention to what they are doing.  Remember what your parents told you about stranger danger:  Don't import forms from total strangers and stick to those forms that you created yourself.

## Load All Resources

The entire app has to be loaded up front, including all libraries and images.  Otherwise the web browser won't know what all it needs to download to the device when saving it for offline use.  HTML doesn't really have a way to declare a list of resources required for offline use of a webpage.  It's somewhat expected that a webpage is, in general, only used online.  Offline use is an edge case that few people think about until they encounter the scenario.

If a complete application uses 10MB of libraries, all 10MB has to be loaded whether or not those libraries will all be used.  So extra care has to be applied to limit the number of libraries and the total file size of all of the libraries.

## Handling Navigation Without Recursion

When a web browser navigates to another page, it usually sends a request to a web server.  If there is no Internet connectivity, the request will fail.  An offline app generally should not navigate outside of the current page (changing `window.location.hash` is probably fine).

Closures in Javascript, while generally useful, hit a weird edge case when navigation is done entirely within the same webpage.  Navigation between app states is accomplished using the same internal function.  This effectively recursively calls the app state function.  Eventually, after thousands of app state changes, the call stack will throw an exception for being too deep.  The fix for this problem is to store the next app state in an object and then wait for a setInterval() that was started when the page loaded to trigger.  The interval callback sees the next state and switches to it.  This helps keep the Javascript call stack small.

## Validating Inputs

The user of the application will inevitably enter information somewhere.  Input validation is something many Javascript apps already do when fields are filled in.  That is still the case.

However, input validation is now also involved when loading data from Local Storage.  Local Storage is one major location where data for the app will be stored.  Previous versions of the application may do things differently or the data could even be modified outside of the app.  Failure to validate all inputs can lead to app level failures where simple reloading of the app won't fix the problems.

## Data Storage/Retrieval

An offline app still needs to store and retrieve data.  Local Storage (`window.localStorage`) is a fairly obvious choice.  And storing JSON encoded data in Local Storage is also a fairly obvious choice.

As mentioned earlier, Local Storage is insecure.  Other webpages on the same domain can access Local Storage as can generic bookmarklets.

Local Storage also has limits.  The maximum size for a single domain is somewhere around 2.5MB to 5MB.

I didn't explore the possibility of using a file Blob as a backend data store.  A real backend file would bypass the Local Storage limits and perhaps improve security a little.  I wanted to target the broadest device support and the File API is a tad bit newer and less broadly supported than I needed.

## No Network Calls

Many people consider that an offline app should never do anything like try to talk to online services.  That includes gathering app usage metrics (e.g. Google Analytics), check for app updates, or anything else.  If the app is offline, it should be completely offline!

The "Export Data" feature of Offline Forms was originally going to have an online data pushing option to SaaS solutions (e.g. Google Sheets) in addition to exporting data as CSV and JSON Lines.  The data entry portion to craft a POST request was too clunky, so I dropped it altogether.

The only time there is a network request in Offline Forms is when the iframe loads the form Preview.  The iframe URL is the same as the parent with a fragment, so it shouldn't be a problem.

## Conclusion

[Offline Forms](https://github.com/cubiclesoft/offline-forms) was an interesting exercise to attempt to build a fully offline app in a web browser.  Obviously, not every web app can be fully offline, but some apps perhaps could be.

The biggest issue with offline web apps is that web browsers don't offer much natively in the area of secure data storage.  Local Storage is useful but is not particularly secure and has limitations.  It's almost like web browsers are supposed to be used for browsing the web.