# Read Frog Google Drive Backup Implementation: Sync Mechanism and Conflict Resolution

> Learn how Read Frog implements Google Drive backup sync, detailing its mechanism for comparing local and remote files, and its conflict resolution strategies to safeguard user configurations.

- Repository: [MengXi/read-frog](https://github.com/mengxi-ream/read-frog)
- Tags: architecture
- Published: 2026-03-07

---

**Read Frog synchronizes user configurations with Google Drive by storing a single JSON file in the app-data folder, comparing timestamps between local and remote versions, and resolving conflicts through deep equality checks or manual user intervention when divergent changes are detected.**

Read Frog, an open-source browser extension available at mengxi-ream/read-frog, implements a robust Google Drive backup system that ensures user configurations remain synchronized across devices. The system handles OAuth authentication with automatic token refresh, maintains a single source of truth in the user's Drive app-data folder, and employs a timestamp-based sync algorithm to detect and resolve conflicts. This article examines the complete implementation, from low-level API interactions to the conflict resolution logic that maintains data consistency.

## OAuth Authentication and Token Management

The authentication layer in [`src/utils/google-drive/auth.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/utils/google-drive/auth.ts) manages the complete OAuth lifecycle. The `authenticateGoogleDriveAndSaveTokenToStorage` function (lines 57-96) builds the Google auth URL, launches the interactive flow, and persists the resulting credentials. The access token, expiry timestamp, and token type are stored under the key `__googleDriveToken` (defined as the constant `GOOGLE_DRIVE_TOKEN_STORAGE_KEY`) using `storage.setItem` (lines 92-95).

Token validation occurs in `getValidAccessToken` (lines 107-118), which retrieves the stored token, validates its shape with Zod, and checks against a 1-minute expiry buffer. If the token is missing or about to expire, the system automatically re-runs the OAuth flow. After successful authentication, `getGoogleUserInfo` (lines 155-175) fetches the user's email address to include in sync metadata.

The React hook `useGoogleDriveAuth` in [`src/hooks/use-google-drive-auth.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/hooks/use-google-drive-auth.ts) wraps this logic, exposing a `react-query` query that automatically invalidates when the token changes in storage, ensuring the UI always reflects the current authentication state.

## Remote Configuration File Handling

All remote operations target a single file named [`read-frog-config.json`](https://github.com/mengxi-ream/read-frog/blob/main/read-frog-config.json) inside the Google Drive app-data folder. The low-level REST API functions reside in [`src/utils/google-drive/api.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/utils/google-drive/api.ts):

- **`findFileInAppData`** (lines 27-45) locates the configuration file and returns its Drive ID
- **`downloadFile`** (lines 64-72) fetches file content given a valid ID
- **`uploadFile`** (lines 94-115) creates new files or overwrites existing ones when an ID is supplied

High-level abstractions in [`src/utils/google-drive/storage.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/utils/google-drive/storage.ts) handle schema migration and metadata extraction. The `getRemoteConfigAndMetaWithUserEmail` function (lines 11-52) obtains the authenticated user's email, downloads the JSON, migrates it to the current schema, and returns both the configuration and its metadata. Conversely, `setRemoteConfigAndMeta` (lines 59-66) uploads the JSON, creating the file in the app-data folder if it does not exist.

## Synchronization Algorithm and Conflict Resolution

The core sync routine in [`src/utils/google-drive/sync.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/utils/google-drive/sync.ts) implements a three-way merge strategy comparing local state, remote state, and the last-known synchronized snapshot. It returns a `SyncResult` indicating success, conflict, or specific error conditions.

### Data Retrieval Phase

The algorithm loads three data sources simultaneously (lines 66-69):

1. **Local configuration** via `getLocalConfigAndMeta`
2. **Last-synced snapshot** via `getLastSyncedConfigAndMeta`
3. **Remote configuration** via `getRemoteConfigAndMetaWithUserEmail`

### First-Time Pairing and Email Validation

If the remote user's email differs from the last-synced metadata (or no snapshot exists), the system handles initial setup (lines 73-90):

- **Remote exists**: Download the remote configuration, replace the local copy, and record the new email in `lastSyncedConfig`
- **Remote missing**: Upload the local configuration to Drive and establish the initial sync record

### Change Detection Logic

The system compares `lastModifiedAt` timestamps to determine the sync direction (lines 94-96):

```typescript
const localChangedSinceSync  = localConfig.meta.lastModifiedAt  > lastSynced.meta.lastModifiedAt;
const remoteChangedSinceSync = remoteConfig && remoteConfig.meta.lastModifiedAt > lastSynced.meta.lastModifiedAt;

```

- **Only local changed** → Upload to Drive
- **Only remote changed** → Download to local storage
- **Neither changed** → No action required
- **Both changed** → Trigger conflict resolution

### Conflict Resolution Strategy

When both sides have changed, the algorithm uses `dequal` to check deep equality (lines 100-129):

- **Identical content**: Treat as "same-changes" and re-upload to normalize timestamps across devices
- **Different content**: Return `{status: "unresolved", data: {base, local, remote}}` to trigger the UI conflict resolver, allowing the user to choose which version to keep

After any successful upload or download, `setLastSyncConfigAndMeta` updates the snapshot with the current timestamp and user email, establishing the new baseline for future comparisons.

## UI Integration and Manual Sync Triggers

Components integrate the backup system through the `useGoogleDriveAuth` hook and the `syncConfig` function. When `authQuery.data?.isAuthenticated` is true, calling `await syncConfig()` executes the full synchronization workflow and returns a typed result indicating the action taken: `"uploaded"`, `"downloaded"`, `"same-changes"`, `"no-change"`, or `"unresolved"`.

```typescript
import { syncConfig } from "@/utils/google-drive/sync";
import { toast } from "sonner";

export function SyncButton() {
  const handleClick = async () => {
    const result = await syncConfig();
    if (result.status === "success") {
      toast.success(`Config ${result.action}`);
    } else if (result.status === "unresolved") {
      toast.warning("Sync conflict – open the conflict resolver.");
    } else {
      toast.error(`Sync failed: ${result.error.message}`);
    }
  };

  return <button onClick={handleClick}>Sync with Google Drive</button>;
}

```

## Summary

- **Authentication Layer**: [`src/utils/google-drive/auth.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/utils/google-drive/auth.ts) manages OAuth flows, token storage under `__googleDriveToken`, and automatic refresh with a 1-minute expiry buffer.
- **Remote Storage**: [`src/utils/google-drive/storage.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/utils/google-drive/storage.ts) abstracts the [`read-frog-config.json`](https://github.com/mengxi-ream/read-frog/blob/main/read-frog-config.json) file in Drive's app-data folder, handling schema migration and email metadata extraction.
- **Sync Engine**: [`src/utils/google-drive/sync.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/utils/google-drive/sync.ts) implements timestamp-based change detection and three-way conflict resolution, delegating to the UI when manual intervention is required.
- **React Integration**: `useGoogleDriveAuth` and `syncConfig` provide declarative hooks for UI components to initiate backup operations and respond to authentication state changes.

## Frequently Asked Questions

### How does Read Frog handle expired OAuth tokens?

The `getValidAccessToken` function in [`src/utils/google-drive/auth.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/utils/google-drive/auth.ts) validates the stored token against a 1-minute expiry buffer. If the token is missing or about to expire, the system automatically re-initiates the OAuth flow through `authenticateGoogleDriveAndSaveTokenToStorage`, ensuring uninterrupted backup operations without manual user intervention.

### What happens when the same configuration is modified on two devices simultaneously?

When both local and remote configurations have changed since the last sync (detected via `lastModifiedAt` timestamps), the algorithm in [`src/utils/google-drive/sync.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/utils/google-drive/sync.ts) first checks deep equality using `dequal`. If the content is identical, it treats the changes as equivalent and re-uploads to normalize timestamps. If the content differs, it returns an "unresolved" status, prompting the UI to display a conflict resolution dialog for manual user selection.

### Where exactly is the backup file stored in Google Drive?

According to the implementation in [`src/utils/google-drive/api.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/utils/google-drive/api.ts), the backup file named [`read-frog-config.json`](https://github.com/mengxi-ream/read-frog/blob/main/read-frog-config.json) is stored in the Google Drive app-data folder, which is a hidden directory accessible only to the Read Frog application. The `findFileInAppData` function (lines 27-45) queries this specific location using the Drive API's spaces parameter set to "appDataFolder".

### How does the sync system detect which version is newer?

The system compares `lastModifiedAt` timestamps between three states: the current local configuration, the remote Drive file, and the last-synced snapshot stored locally. In [`src/utils/google-drive/sync.ts`](https://github.com/mengxi-ream/read-frog/blob/main/src/utils/google-drive/sync.ts) (lines 94-96), boolean flags `localChangedSinceSync` and `remoteChangedSinceSync` determine whether to upload, download, or flag a conflict based on whether each side's timestamp exceeds the baseline snapshot timestamp.