Sync & Auth API
The Sync and Auth APIs manage cloud synchronization and user authentication through the backend bridge.
Cloud Sync API
The cloudSync namespace on BackendAPI provides methods to connect, sync, manage encryption, and listen for status or remote-change events. The namespace is backend.cloudSync.* — not backend.sync.*.
cloudSync.getStatus()
Get the current cloud sync status.
const status = await backend.cloudSync.getStatus();
// { status: "idle", lastSyncedAt: null, filesCount: 0, cursor: 0, connectedDevices: 0 }Returns: CloudSyncStatusInfo — a snapshot of the current sync state.
cloudSync.triggerSync()
Trigger a manual cloud sync (push + pull).
const result = await backend.cloudSync.triggerSync();Returns: CloudSyncStatusInfo — the sync state after the operation completes.
cloudSync.connect()
Connect the WebSocket for real-time sync notifications.
await backend.cloudSync.connect();Returns: Promise<void>
cloudSync.disconnect()
Disconnect the WebSocket.
await backend.cloudSync.disconnect();Returns: Promise<void>
cloudSync.setupEncryption(password)
Set up encryption password for the workspace. Called once when the user first configures sync.
const ok = await backend.cloudSync.setupEncryption("my-password");| Parameter | Type | Description |
|---|---|---|
password | string | The user's encryption passphrase |
Returns: Promise<boolean>
cloudSync.verifyPassword(password)
Verify the user's encryption password against the stored verification hash.
const valid = await backend.cloudSync.verifyPassword("my-password");| Parameter | Type | Description |
|---|---|---|
password | string | The passphrase to verify |
Returns: Promise<boolean>
cloudSync.armEncryption(password)
Arm end-to-end encryption with a single password gesture, branching on the workspace's current encryption state:
Workspace has no encryption: derive a fresh key and claim the workspace (
outcome: "created").Workspace has encryption: verify the password before arming (
outcome: "verified"); a wrong password returnsoutcome: "wrong-password"and overwrites nothing.
const result = await backend.cloudSync.armEncryption("my-password");
if (result.outcome === "created" || result.outcome === "verified") {
// Encryption is now armed; runtime observers are notified automatically.
}| Parameter | Type | Description |
|---|---|---|
password | string | The passphrase to arm with |
Returns: Promise<ArmEncryptionResult>
cloudSync.getConflicts()
Get pending sync conflicts.
const conflicts = await backend.cloudSync.getConflicts();Returns: Promise<CloudConflictItem[]>
cloudSync.resolveConflicts(resolutions)
Resolve pending conflicts by choosing a per-file strategy.
await backend.cloudSync.resolveConflicts(
new Map([
["path/to/file.md", "use-local"],
["path/to/other.md", "use-server"],
])
);| Parameter | Type | Description |
|---|---|---|
resolutions | Map<string, "use-local" | "use-server" | "save-both"> | Per-file resolution strategy |
Returns: Promise<void>
cloudSync.onStatusChanged(callback)
Listen for cloud sync status changes.
const unsubscribe = backend.cloudSync.onStatusChanged((status) => {
console.log("Sync status:", status.status);
});
// Later: stop listening
unsubscribe();| Parameter | Type | Description |
|---|---|---|
callback | (status: CloudSyncStatusInfo) => void | Called whenever the sync status changes |
Returns: () => void — an unsubscribe function.
cloudSync.onRemoteChange(callback)
Listen for real-time remote change notifications pushed over the WebSocket.
const unsubscribe = backend.cloudSync.onRemoteChange((changes) => {
console.log("Remote changes:", changes.length);
});
// Later: stop listening
unsubscribe();| Parameter | Type | Description |
|---|---|---|
callback | (changes: CloudConflictItem[]) => void | Called when the server pushes change notifications |
Returns: () => void — an unsubscribe function.
CloudSyncStatusType
type CloudSyncStatusType = "idle" | "syncing" | "synced" | "error" | "conflict" | "offline";| Value | Description |
|---|---|
"idle" | No sync in progress, no previous sync |
"syncing" | Sync operation is currently running |
"synced" | Last sync completed successfully |
"error" | Last sync failed |
"conflict" | Sync found conflicting changes |
"offline" | Device is offline; sync is paused |
CloudSyncStatusInfo
interface CloudSyncStatusInfo {
status: CloudSyncStatusType;
lastSyncedAt: string | null;
filesCount: number;
cursor: number;
connectedDevices: number;
error?: string;
}| Field | Type | Description |
|---|---|---|
status | CloudSyncStatusType | Current sync state |
lastSyncedAt | string | null | ISO 8601 timestamp of last successful sync, or null if never synced |
filesCount | number | Number of files included in the last sync |
cursor | number | Server-side change log cursor position |
connectedDevices | number | Number of devices currently connected to the workspace |
error | string (optional) | Error message when status is "error" |
defaultCloudSyncStatus
const defaultCloudSyncStatus: CloudSyncStatusInfo = {
status: "idle",
lastSyncedAt: null,
filesCount: 0,
cursor: 0,
connectedDevices: 0,
};ArmEncryptionResult
type ArmEncryptionResult =
| { outcome: "created"; saltHex: string }
| { outcome: "verified"; saltHex: string }
| { outcome: "wrong-password" }
| { outcome: "error"; message: string };| Outcome | Description |
|---|---|
"created" | No prior encryption; a fresh key was derived and the workspace is now armed |
"verified" | Workspace already had encryption; password matched and keys are armed |
"wrong-password" | Workspace already had encryption but the password did NOT match — nothing was overwritten |
"error" | A prerequisite was missing or a network/derivation failure occurred; message explains |
saltHex (present on created and verified) is the hex-encoded derivation salt that was actually used. Callers should persist this via persistWorkspaceKey(password, salt) for cold-start re-arm.
CloudConflictItem
interface CloudConflictItem {
path: string;
encryptedPath: string;
localVersion: number;
serverVersion: number;
type: "concurrent-edit" | "edit-delete" | "delete-edit";
}Auth API
The auth namespace on BackendAPI manages user authentication state — login, logout, and state change events. Better Auth uses a browser handoff and single-use OTT to establish the client session.
auth.getState()
Get the current authentication state.
const state = await backend.auth.getState();
// { isAuthenticated: false, user: null }Returns: AuthState — the current auth state.
auth.login()
Start the Better Auth login flow. Desktop opens the system-browser handoff with the current app's deep-link scheme; the web adapter redirects to the allowlisted HTTPS handoff.
await backend.auth.login();Returns: void
auth.logout()
Logout and clear tokens.
await backend.auth.logout();Returns: void
auth.onStateChanged(callback)
Listen for authentication state changes.
const unsubscribe = backend.auth.onStateChanged((state) => {
if (state.isAuthenticated) {
console.log("Logged in as", state.user?.name);
}
});
// Later: stop listening
unsubscribe();| Parameter | Type | Description |
|---|---|---|
callback | (state: AuthState) => void | Called whenever the auth state changes |
Returns: () => void — an unsubscribe function.
AuthUser
interface AuthUser {
id: string;
email: string;
name: string;
picture?: string;
}| Field | Type | Description |
|---|---|---|
id | string | Unique user identifier |
email | string | User's email address |
name | string | Display name |
picture | string (optional) | Profile picture URL |
AuthState
interface AuthState {
isAuthenticated: boolean;
user: AuthUser | null;
}| Field | Type | Description |
|---|---|---|
isAuthenticated | boolean | Whether the user is currently logged in |
user | AuthUser | null | The authenticated user, or null when logged out |
defaultAuthState
const defaultAuthState: AuthState = {
isAuthenticated: false,
user: null,
};Adapter Implementations
MockAdapter
The mock adapter provides a fully functional in-memory implementation for tests and Storybook.
Cloud sync behavior:
getStatus()returns a clone of the internalcloudSyncStatusstatetriggerSync()transitions through"syncing"→"synced"with a 500ms delay, counting all files (messages + inbox notes)connect()/disconnect()are no-ops that resolve immediatelysetupEncryption()/verifyPassword()/armEncryption()are in-memory stubsgetConflicts()returns an empty array by defaultonStatusChanged()fires whenevertriggerSync()completes orcontrols.setCloudSyncStatus()is calledonRemoteChange()fires whencontrols.triggerRemoteChange()is called
Auth behavior:
login()simulates a 500ms delay then sets a mock user (id: "mock-user-001",email: "[email protected]",name: "Mock User")logout()resets todefaultAuthStateonStateChanged()fires on login and logout
TauriAdapter
Fully implements cloudSync: connects to the sync server with a short-lived Better Auth service JWT, drives push/pull cycles, manages the WebSocket via the @takazudo/cloud-sync client, and handles the per-app OTT deep-link flow. The adapter reads/writes encrypted blobs and persists the device cursor across sessions.
RestAdapter
Fully implements cloudSync: same contract as TauriAdapter, backed by HTTP/SSE calls to the sync server REST API. Used in pnpm dev:rest mode and on web (browser deploy). The Better Auth HTTPS handoff, service-JWT refresh hooks, and WebSocket connections are fully wired.