aquabase 0.6.6
aquabase: ^0.6.6 copied to clipboard
Flutter SDK for Aquabase — encrypted real-time database with offline sync, tags, and binary transport.
Aquabase Flutter SDK #
Encrypted real-time database for Flutter. Offline-first with automatic cloud sync, binary transport, and zero code generation.
Quick Start #
import 'package:aquabase/aquabase.dart';
final app = await Aquabase.init(
projectId: 'your_project_id',
apiKey: 'aq_pub_...',
);
// Database (instant local cache + background sync)
await app.collection('users').doc('u1').set({'name': 'Ana', 'age': 25});
final user = await app.collection('users').doc('u1').get();
// Auth
final result = await app.auth.login('user@example.com', 'password123');
// Storage
await app.storage.upload('avatars', 'photo.jpg', bytes);
// Logs
await app.logs.connect();
app.logs.info('db', 'User created account');
How It Works #
- All reads/writes go to the local cache — instant, no network latency
- When online, changes sync to the server in background
- When offline, failed ops queue to disk automatically
- On reconnect, the queue flushes to the server — no data loss
Collections #
final users = app.collection('users');
await users.doc('u1').set({'name': 'Ana'});
final user = await users.doc('u1').get();
await users.doc('u1').delete();
Bulk Operations #
await users.bulkSet({
'u1': {'name': 'Ana'},
'u2': {'name': 'Luis'},
'u3': {'name': 'Carlos'},
});
final results = await users.bulkGet(['u1', 'u2', 'u3']);
await users.bulkDelete(['u1', 'u2', 'u3']);
Real-Time #
Watch a document or a query for changes — emits current data immediately, then on every update from any device.
// Watch a single document
final sub = users.watch('u1').listen((user) {
if (user != null) renderUser(user);
});
// Watch a query (Enterprise API)
final sub2 = users.where('status', isEqualTo: 'active').watch().listen((results) {
results.forEach((id, data) => print('$id: $data'));
});
// Stop watching
sub.cancel();
sub2.cancel();
Indexes #
Indexes are automatic. The first time you use .where('field', ...), the SDK registers that field as indexed and syncs it to the server. From that point on, every set() auto-generates tags for queried fields. No manual configuration needed.
Note: When a field is auto-registered, the SDK prints a warning:
[Aquabase] Index auto-created: "field" on "collection". Existing documents need a re-write (set()) to become queryable by the new field.
final attendances = app.collection('attendances');
// .where() auto-registers 'date' and 'type' as indexed fields
final results = await attendances
.where('date', isEqualTo: '2026-03-18')
.where('type', isEqualTo: 'present')
.get();
// From now on, set() auto-generates tags for 'date' and 'type'
await attendances.set('a1', {
'date': '2026-03-18',
'studentId': 'stu_001',
'type': 'present',
'notes': 'On time',
});
// Real-time query
final sub = attendances
.where('date', isEqualTo: '2026-03-18')
.watch()
.listen((results) {
results.forEach((id, data) => print('$id: $data'));
});
sub.cancel();
Range Queries (campos ordenados) #
Para campos donde necesitas >, < o BETWEEN (fechas, scores, precios), usa operadores de rango. El SDK selecciona automáticamente el índice BTreeMap en lugar del HashMap.
final scores = app.collection('scores');
// Registra 'score' como campo range en el primer uso
final results = await scores
.where('schoolId', isEqualTo: 'school_01') // filtro equality
.where('score', isGreaterThanOrEqual: 80) // filtro range
.where('score', isLessThanOrEqual: 100)
.get();
// Range put: set() genera automáticamente las entradas correctas
await scores.set('sc1', {'schoolId': 'school_01', 'score': 95});
Operadores disponibles:
| Operador | Ejemplo |
|---|---|
isGreaterThan |
isGreaterThan: 80 |
isGreaterThanOrEqual |
isGreaterThanOrEqual: 80 |
isLessThan |
isLessThan: 100 |
isLessThanOrEqual |
isLessThanOrEqual: 100 |
isBetween |
isBetween: [80, 100] |
Datos históricos: Los documentos escritos antes de registrar un campo como
rangeno se re-indexan automáticamente. Llama aset()de nuevo sobre esos docs para que aparezcan en queries de rango.
Local Collections #
Keep data only on the device — no server sync, no offline queue. Useful for caching data from external sources (Firebase, REST APIs, etc).
// Normal collection — syncs to server
final attendances = app.collection('attendances');
// Local collection — data stays on the device only
final cache = app.collection('students_cache', local: true);
// Same API, zero server interaction
await cache.doc('s1').set({'name': 'Ana', 'qr': 'ABC123'});
final student = await cache.doc('s1').get();
await cache.doc('s1').delete();
// Bulk operations also stay local
await cache.bulkSet({
's1': {'name': 'Ana'},
's2': {'name': 'Luis'},
});
Local collections use the same L1 (SIEVE memory) + L2 (disk) cache as normal collections. They persist across app restarts but never touch the network.
Note: A collection name cannot be both local and remote simultaneously. Attempting to create
collection('users')andcollection('users', local: true)will trigger an assertion error.
Date Handling #
DateTime objects are automatically serialized as milliseconds since epoch (UTC). On read, timestamps are returned as int — convert in your app:
// Write
await users.doc('u1').set({
'name': 'Ana',
'createdAt': DateTime.now(),
});
// Read
final user = await users.doc('u1').get();
final date = DateTime.fromMillisecondsSinceEpoch(user!['createdAt'], isUtc: true);
This format is cross-platform compatible with the Node/Web SDK.
Offline-First #
Works without connection. Writes go to local cache and queue for sync.
await app.collection('users').doc('u1').set({'name': 'Ana'});
// Check pending ops
print(app.pendingOfflineOps);
// On reconnect, queue flushes automatically
// Or flush manually:
await app.flushOfflineQueue();
Encryption #
final app = await Aquabase.init(
projectId: 'your_project_id',
apiKey: 'aq_pub_...',
enableCrypto: true, // AES-256-GCM
);
Custom Server URL #
final app = await Aquabase.init(
projectId: 'your_project_id',
apiKey: 'aq_pub_...',
url: 'http://localhost:3280',
);
Auth #
// Email/password
final result = await app.auth.register('user@example.com', 'password123');
final login = await app.auth.login('user@example.com', 'password123');
// OAuth (Google, GitHub)
final google = await app.auth.signInWithOAuth(OAuthProvider.google, googleIdToken);
final github = await app.auth.signInWithOAuth(OAuthProvider.github, githubCode);
// Reconnect with user identity
final userApp = await Aquabase.init(
projectId: 'your_project_id',
apiKey: 'aq_pub_...',
jwt: result.token,
);
Storage #
await app.storage.createBucket(name: 'avatars', public: true);
final bytes = await File('photo.jpg').readAsBytes();
await app.storage.upload('avatars', 'user1.jpg', bytes, contentType: 'image/jpeg');
final files = await app.storage.listFiles('avatars');
final data = await app.storage.download('avatars', 'user1.jpg');
await app.storage.deleteFile('avatars', 'user1.jpg');
Logs #
await app.logs.connect();
app.logs.info('db', 'User created account');
app.logs.warn('auth', 'Invalid password attempt', source: '192.168.1.1');
app.logs.error('ws', 'Connection timeout');
final records = await app.logs.query(LogQueryOptions(
since: DateTime.now().subtract(Duration(hours: 1)).microsecondsSinceEpoch * 1000,
minLevel: LogLevel.warn,
channel: 'auth',
limit: 100,
));
Crash Capture #
Enabled by default when connect() is called. Unhandled exceptions and Flutter errors are sent to the server automatically.
await app.logs.connect(); // Crashes captured automatically
API Reference #
Aquabase #
| Method | Description |
|---|---|
Aquabase.init(...) |
Initialize with named parameters |
Aquabase.instance |
Global access after init |
collection(name) |
Get a collection (synced) |
collection(name, local: true) |
Get a local-only collection |
isConnected |
WebSocket connection status |
pendingOfflineOps |
Pending offline operations count |
flushOfflineQueue() |
Manually sync pending ops |
connectionState |
Stream of connection changes |
deniedOperations |
Stream of blockIds rejected by security rules |
flushResults |
Stream of FlushResult after each sync |
dispose() |
Release resources |
Init Options #
| Parameter | Type | Default | Description |
|---|---|---|---|
projectId |
String |
— | Project ID (required) |
apiKey |
String |
— | API key (required) |
url |
String |
Aquabase cloud | Server URL |
jwt |
String |
null |
JWT for authenticated users |
enableCrypto |
bool |
false |
Enable AES-256-GCM encryption |
enableCache |
bool |
true |
Enable L1/L2 local cache |
Collection #
| Method | Description |
|---|---|
doc(id) |
Document reference |
set(id, data) |
Write with auto-tags for indexed fields |
bulkSet(docs) |
Batch write |
bulkGet(ids) |
Batch read |
bulkDelete(ids) |
Batch delete |
where(f, val) |
Query Builder → .get() or .watch() |
watch('id') |
Real-time doc stream |
syncEvents |
Stream of sync events for custom listeners |
isLocal |
Whether the collection is local-only |
dispose() |
Release resources |
Document #
| Method | Description |
|---|---|
set(data) |
Write to cache + sync (or cache-only if local) |
get() |
Read from cache, fallback server (cache-only if local) |
delete() |
Delete from cache + sync (or cache-only if local) |
Auth #
| Method | Description |
|---|---|
app.auth.register(email, password) |
Register user, returns JWT + uid |
app.auth.login(email, password) |
Login user, returns JWT + uid |
app.auth.signInWithOAuth(provider, credential) |
OAuth login (Google/GitHub), auto-registers |
app.auth.refresh(token) |
Refresh an expired JWT |
Storage #
| Method | Description |
|---|---|
app.storage.listBuckets() |
List all buckets |
app.storage.createBucket(name, compression?, pub?) |
Create a new bucket |
app.storage.deleteBucket(name) |
Delete a bucket and all its files |
app.storage.listFiles(bucket) |
List files in a bucket |
app.storage.upload(bucket, path, bytes, type?) |
Upload a file (overwrites) |
app.storage.download(bucket, path) |
Download file as bytes |
app.storage.deleteFile(bucket, path) |
Delete a file |
Logs #
| Method | Description |
|---|---|
app.logs.connect() |
Connect + enable crash capture |
app.logs.info(channel, msg) |
Push info-level log |
app.logs.warn(channel, msg) |
Push warn-level log |
app.logs.error(channel,msg) |
Push error-level log |
app.logs.flush() |
Flush buffer to server |
app.logs.query(opts?) |
Query stored logs |
app.logs.dispose() |
Flush + disconnect |
Architecture #
| Component | Implementation |
|---|---|
| Serialization | MsgPack — DateTime → ms epoch auto-conversion |
| L1 Cache | SIEVE eviction (NSDI '24) — lazy promotion |
| L2 Storage | CompactCache — read-only snapshot + append-only disk |
| Cloud Sync | WebSocket with auto-reconnect + backoff |
| Offline Queue | Persisted to disk, auto-flush on reconnect |
| Encryption | AES-256-GCM |
License #
Proprietary