bothy (Flutter)

Serverless, local-first sync engine that uses the user's own Google Drive as the transport.

⚠️ Early development. The API may still change between minor versions.

Bothy lets Flutter apps sync structured data between phones, desktops and embedded devices without a server of their own. Every device keeps a local SQLite database and works offline; devices exchange immutable files through the user's own Google Drive. Concurrent edits to different fields merge automatically, and the files on Drive can be end-to-end encrypted.

The engine is the Rust core, bundled as a native library. Building an app that uses this package needs a Rust toolchain (rustup); the native code is compiled for each platform during the Flutter build. Supported: Android, iOS, macOS, Linux and Windows.

Usage

import 'package:bothy/bothy.dart';

await Bothy.initialize();
final db = await Bothy.open(
  path: '${supportDir.path}/notes.db',
  appId: 'com.example.notes',
  remote: BothyRemoteSpec.googleDriveAppData(
    (forceRefresh) => mySignIn.accessToken(refresh: forceRefresh),
  ),
);
await db.initOrJoin();

final notes = db.collection('notes');
await notes.put('n1', {'title': 'Hello', 'done': false});
await notes.patch('n1', {'done': true});
final note = await notes.get('n1');

notes.watch().listen((change) => refresh(change.id));
db.startAutoSync();

Google sign-in

The package never signs in by itself. The app runs the OAuth flow (for example with google_sign_in) and gives Bothy a callback that returns an access token with the https://www.googleapis.com/auth/drive.appdata scope (or drive.file for BothyRemoteSpec.googleDriveFolder), or null when the user is signed out. Bothy calls it with forceRefresh: true after Drive rejects a token.

Encryption

final db = await Bothy.open(..., encrypt: true, password: password); // first device
final db = await Bothy.open(..., password: password);                // other devices

Keep db.exportKey() in secure storage (for example flutter_secure_storage) and open with key: next time, so the user is not asked for the password on every start. A wrong password throws BothyException with BothyErrorKind.wrongPassword; an encrypted database opened without a secret is locked: local reads and writes work, sync throws BothyErrorKind.keyRequired until unlock. changePassword changes it on every device.

Conflicts

Writes that lose to a newer write from another device are kept in a log: db.conflicts(since: ...) and db.clearConflicts().

Testing

flutter test runs the engine on the host. Build the native library first:

cargo build --release --manifest-path rust/Cargo.toml
flutter test

License

MIT OR Apache-2.0 (see LICENSE).

Libraries

bothy
Bothy: serverless, local-first sync over the user's own Google Drive.