Unsorted AI adopts a local-first approach to ensure successful data collection in all scenarios. This is achieved by using a react state management library called Legend State (make this a link).

Application

Database sync layer

The following code creates a re-useable template for performing offline sync with supabase using local storage. Syncing is retried infinite times with exponential backoff to ensure data is eventually synced.

export const supabaseSynced = configureSynced(syncedSupabase, {
	persist: {
		plugin: ObservablePersistLocalStorage,
		retrySync: true,
	},
	supabase,
	changesSince: "last-sync",
	fieldCreatedAt: "created_at",
	fieldUpdatedAt: "updated_at",
	fieldDeleted: "deleted",
	retry: {
		infinite: true,
	},
});

We can create a state store that binds its data to our offline sync system.

export const devices$ = observable(
	supabaseSynced({
		collection: "device",
		select: (from) => from.select("*").eq("user_id", session$.user.id.get()!),
		supabase,
		actions: ["create", "update", "read", "delete"],
		waitFor: session$.user.id.get(),
		persist: {
			name: "device",
		},
	})
);

This code sets up a new state synced to the database ‘device’ table for the current user. We wait for the session to exist before trying to sync, and we set a persist key of ‘device’ for local storage. Changes to each row will be synced between the users device and the database. This allows us to insert pseudo rows into the database while the device is offline, and when it comes back online the rows will be inserted.

Here is how the device$ store is used in the application to register new devices.

import * as Device from "expo-device";
import * as Application from "expo-application";
import { session$ } from "~/lib/state/session";
import { devices$ } from "~/lib/state/devices";
 
// Mapping helper for human readable device types
const DEVICE_TYPES = {
	0: "UNKNOWN",
	1: "PHONE",
	2: "TABLET",
	3: "TV",
	4: "DESKTOP",
} as const;
 
  
// Determine whether the current device is iOS or Android
export async function getDeviceId() {
	return Device.osName === "iOS"
	? await Application.getIosIdForVendorAsync()
	: Application.getAndroidId();
}
 
// Main function
export async function registerDevice() {
// Require the user is logged in to register device
const userId = session$.user.id.get();
if (!userId) throw new Error("User not authenticated");
 
// Use the device id (acts like a serial number)
const id = await getDeviceId();
if (!id) throw new Error("Device ID not found");
 
// Check local storage to see if device already exists, return early if it does
const existingDevice = devices$[id].get();
if (existingDevice) {
	return existingDevice.id;
}
 
// Get the device type we will use later to label the data
const device_type = Device.deviceType ? DEVICE_TYPES[Device.deviceType!] : null;
if (!device_type) {
	throw new Error("Device type not found");
}
 
  
// Create the new data to be inserted into the database
const params = {
	id,
	user_id: userId,
	model_name: Device.modelName ?? "",
	name: Device.deviceName ?? "",
	device_type,
	brand: Device.brand ?? "",
	created_at: null,
	updated_at: null,
	deleted: false,
};
devices$[id].assign(params);
 
return id;
}

Arbitrary sync layer

The sync system is used for other external requests that need to be synced as well

export const permissions$ = observable<Record<string, string>>(
	synced({
		initial: {},
		persist: {
			plugin: ObservablePersistLocalStorage,
			retrySync: true,
			name: "permissions",
		},
	})
);

This syncs the device permissions (eg allow location) to local storage which is used for more efficient and synchronous permission checking.