· 6 min read
Offline-First PWA
I needed an app that would allow users to submit forms while offline and/or with expired authorization credentials. But the app is not available for anyone to access so the user must be manually registered and the user must first login while online once before using the app. Similar to a guest mode on e-commerce or other websites but the app is not open for the public and the app is expected to work without network or authorization to the backend; hence it works offline first.
The users for this app are delivery drivers who operate in remote areas for extended trips so a worst case scenario is losing authorization, going offline then stuck at the login screen unable to use the app to record deliveries.
Solution
The solution revolves around my authStore
and the decision I made to treat both the user’s info and their authorization as independent entities. In a typical app with user login the two would be all or nothing and when the authorization token is lost the user is usually logged out and forced to login again to verify their identity. I decided if the user had logged in once already then I can allow them to continue in the app and they can fill an outbox with items but can not submit the items until they verify their identity again by logging in. Plus, this outbox pulls double-duty for authenticated users who just happen to be offline.
Handling Authorization
First step, I had to keep info about the user and the user’s authorization token in the browser’s local storage.
Then I set up an auth
store with Pinia and defined basic actions. I’m using Directus for the backend of this project. Of note, user
is a computed value using the storedUser
so that even after a page refresh we can seamlessly load the user from the browser’s storage.
Login Page
The app’s router can now use the authStore
and only if there is no user
do we direct the user to the login page. This allows a user that has previously logged in to continue using the app without an auth token. This would now be an anonymous mode or something but not a guest mode like you would see on some e-commerce websites.
Later, have to handle some problems we just introduced if an unauthenticated user attempts to GET something from a protected endpoint.
A user may try to login but then become offline, so to prevent being stuck on the login screen we add a link to allow the user to return to the main app. Alternatively, we could have offered a modal for users to re-login which would not lock the user in a route they cannot navigate out of otherwise. Or someone could not design their login page like mine which hid all navigation by default
.Elsewhere we can write logic to distinguish between a user with valid authorization or not using isLoggedIn
.
Handling Authorization Errors While Offline
This may be enough boilerplate for some offline apps; however, for this particular app I needed more as all API endpoints including GET endpoints were secured. Therefore, without authorization the app would not be able to fetch what it needs to function. Here the service worker again transparently does double service to provide a cache for us when the user is offline or when the user is currently unauthorized.
I was leaning on Workbox for this app’s service worker and to make it respond with cached results due to authorization errors I had to add a small plugin to the caching strategy.
Entry Point to Offline-First
Finally, we modify App.vue
to attempt to update the current storedUser
on initial load of the app. If that fails due to an expired or missing auth token then the app will continue forward using the last known user from the browser’s storage and if that fails then we hit the login page per the router’s setup. And that’s it, the user can now use the app in an offline-first manner.
Next Steps
Now, this particular setup works for my usecase but it is not a panacea for all offline-first apps.
Firstly, I have the benefit of an app with closed registration which reduces the number of edge cases. Otherwise, I may have to consider handling deleting the current outbox when the user voluntarily logs out of the app as a new user could inherit an existing outbox when they login to a shared device.
Secondly, this design relies on a service worker but an alternative solution could implement a local cache.
For a different app you may have to adapt this tutorial for your particular requirements.
Conclusion
I hope this tutorial helps others to develop more apps that do not depend on an ever-present internet access. Also, I am grateful for being afforded the time to develop this app which actually provides real-world benefit.
- pwa