Our previous blog post delved into setting up a basic synchronisation between the local WatermelonDB and a remote Supabase project. This setup enables offline-first operations on our mobile application.

You can find the previous blog post here.

The corresponding demo application source code can be found here.

In this blog post, we'll advance our discussion by exploring a method for managing user authentication in tandem with our offline data.

Keep in mind that to simplify our example and focus on our method's architecture rather than the details of Supabase authentication, we'll simulate the authentication process. Instead of conducting actual authentication with Supabase (which we plan to cover in a future post), we'll have the user simply enter a Username. We will consider the presence of this Username as an indication that the user is authenticated.

Are you looking to build an offline first Expo app or need support integrating WatermelonDB into your existing Expo and Supabase app? Book a free call with one of our experts to find out how Morrow can help.

Develop an offline-first app with Supabase, Expo and WatermelonDB - Handling authentication

Embracing an offline-first strategy means our user's data will be stored locally in WatermelonDB, ensuring availability even without a connection to our backend, the ultimate source of truth. However, it's crucial to prevent data mixing between different users who might log into our application.

Our strategy involves clearing the local database when a user logs out. This step ensures that data from one user is not accessible to another who may later log into the app. Importantly, we should not clear the remote database; we want to ensure that the data remains accessible when the user logs back in or accesses it from a different device.

Luckily, WatermelonDB has the needed functionality for this:

Database.unsafeResetDatabase()

You can find out details about it here.

Introducing Expo Router

As our application evolves beyond its initial single-screen setup, we've transitioned to using Expo Router to simplify navigation across multiple screens.

While we won't delve into the setup of the Expo Router here (you can refer to its documentation or our blog post "What is Expo Router" for that), we'll outline our routing structure for this example:

Here's what each part of the structure signifies:

  • app/_layout.tsx will encapsulate our global state and styling.
  • app/login.tsx serves as the login screen.
  • app/(app) contains a set of routes that are protected, meaning they are inaccessible without user authentication.
  • app/(app)/_layout.tsx acts as a global wrapper for these protected routes, where we'll carry out our authentication checks.

Finally, app/(app)/index.tsx is the new home page, replacing the former App.tsx, and it displays the user's favourite board games.

Read: "Exploring React Native Bottom Sheet: Implementation, Examples, and Setup."

Managing Our Authentication State

Next, let's discuss our authentication state management. To maintain this state, we will utilise a React Context for simplicity and familiarity. While at Morrow, we typically prefer mobx for state management, for this demonstration, using Context will suffice.

Thus, the Context will supply the current user's username, along with a method to login and another to logout.

Below is the provider that maintains and manages the authentication state.

Here's a simple breakdown of its functionality:

Upon mounting, it attempts to retrieve the current username from persistent storage, allowing our (simulated) "session" to persist across application restarts.

It's worth noting that we opt to use WatermelonDB to store the username instead of the more common AsyncStorage. While this isn't a requirement, it offers benefits like seamless integration and synchronisation with the database's overall state. You can delve deeper into this choice here: WatermelonDB Advanced Local Storage.

The login function is straightforward: it updates the state and stores the username for persistence.

The logout function is where our approach truly shines. Changing the state to "logged out" triggers a thorough cleanup of the database using the db.unsafeResetDatabase() operation.

After the database is "reset," invoking WatermelonDB's "sync" function won't push any changes or deletions to the backend, ensuring our backend data remains intact. However, implementing additional safeguards to prevent syncing when the user is not authenticated would be a prudent measure to avoid unintended consequences.

Finally, we have our AuthGuard:

This security mechanism can envelop and safeguard our screens. Should a user not be authenticated—meaning, in our simulation, they haven't set a username—they are redirected to the Login page.

We implement this safeguard within the app/(app)/_layout.tsx, ensuring it automatically extends its protection to all the routes in the protected group.

Our AuthContextProvider, on the other hand, wraps the whole project in our app/_layout.tsx file.

Synchronising with the Backend Based on Username

Now, we're set to delve into the synchronisation aspect.

Initially, we've ensured that the user's logout action clears the database. This way, the local database maintains only the logged-in user's entries.

Our next step is to tag changes pushed to the backend with the user's identity, ensuring records are associated with their respective owner. Similarly, when fetching updates from the backend, the system should retrieve only the data relevant to the currently logged-in user.

This process requires incorporating the username into both the pushing and pulling mechanisms of WatermelonDB's sync function. It's important to note that we're not altering the local database schema; it doesn't need to track record ownership. The backend exclusively manages this detail.

Here's how we've adjusted the sync function to accommodate this approach.

The changes since our previous version of sync are the following.

We read the currently logged-in user, and we abort if there’s no authenticated user.

We inserted an extra parameter p_record_ower in our Supabase pull and push changes.

This additional parameter must be managed in the backend to ensure the correct association of each record with its owner. This step is crucial because, in our example, we're 'simulating' the authentication process. In a genuine authentication scenario (which we will explore in a future post), we could leverage the user_id provided by Supabase authentication to streamline this process.

Read: "What’s New in Expo SDK 50: New Features and Updates."

Adjusting Supabase Functions for Record Owner Management

To accommodate our setup, it's essential to modify our Supabase functions to manage the additional p_record_owner field transmitted to Supabase.

Modifying the Table Schema

First, starting from the bottom to the top, we have to update the board_games table schema to support a record_owner. In our example, here is a string that will hold the username.

https://github.com/morrowdigital/watermelondb-plugin-example/blob/main/sql/2_create_board_games_table.sql

Revamping the Game Creation and Update Functions

The function for creating entries must now be equipped to capture and retain the record owner's information:

https://github.com/morrowdigital/watermelondb-plugin-example/blob/main/sql/3_create_create_game_function.sql

Our update function has a similar update:

https://github.com/morrowdigital/watermelondb-plugin-example/blob/main/sql/4_create-update-game-function.sql

And our Pull and Push functions.

The pull function should get only the records of the record owner:

https://github.com/morrowdigital/watermelondb-plugin-example/blob/main/sql/5_create_pull_function.sql

The push function:

https://github.com/morrowdigital/watermelondb-plugin-example/blob/main/sql/6_create_push_function.sql

And the final result:

Wrap-Up

In this post, we built upon our previous discussion where we established a fundamental sync between WatermelonDB and the Supabase backend.

We explored a straightforward method to manage synchronisation in a multi-user environment. Our strategy involved tagging each data record with its owner's identifier. During the push and pull operations, this owner information is transmitted to ensure data consistency and isolation.

We also covered the critical step of clearing the local database whenever a user logs out, maintaining data privacy and integrity.

You can check out our sample code for a deeper understanding:

GitHub - WatermelonDB Plugin Example.

Happy coding!

Are you looking to build an offline first Expo app or need support integrating WatermelonDB into your existing Expo and Supabase app? Book a free call with one of our experts to find out how Morrow can help.
Want experts on your side to drive forward your project?
Need a second opinion on something?
We’re here to help!
Find out more
More insights