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

  1. All reads/writes go to the local cache — instant, no network latency
  2. When online, changes sync to the server in background
  3. When offline, failed ops queue to disk automatically
  4. 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
final sub2 = users.where('status', eq: 'active').watch().listen((results) {
  results.forEach((id, data) => print('$id: $data'));
});

// Stop watching
sub.cancel();
sub2.cancel();

Indexes

Los índices son automáticos. Al usar .where() por primera vez en un campo, el SDK lo registra como indexado y lo sincroniza con el servidor. De ahí en adelante, cada set() genera los tags correctos automáticamente.

Además, al inicializar, el SDK descarga del servidor la lista de campos ya indexados por otras apps del mismo proyecto. Esto garantiza que si QRline registra studentId como índice, Educanet escribirá con ese tag desde el primer set(), sin necesidad de coordinar manualmente.

Igualdad

final attendances = app.collection('attendances');

// Un día exacto
final results = await attendances
    .where('date', eq: '2026-03-18')
    .where('type', eq: 'entrada')
    .get();

// Watch en tiempo real
attendances
    .where('date', eq: '2026-03-18')
    .watch()
    .listen((results) => print(results.length));

Rangos

Usa from/to para rangos inclusivos (el caso más común). El SDK selecciona automáticamente el índice BTreeMap.

final scores = app.collection('scores');

// Scores entre 80 y 100
final high = await scores
    .where('score', from: 80, to: 100)
    .get();

// Asistencias del mes de marzo
final march = await attendances
    .where('date', from: '2026-03-01', to: '2026-03-31')
    .where('studentId', eq: 'stu_001')  // filtro adicional de igualdad
    .get();

// Un solo día (usa eq, no range)
final today = await attendances
    .where('date', eq: '2026-03-20')
    .get();

Todos los operadores disponibles:

Parámetro Tipo Descripción
eq cualquier Igualdad exacta
from num / String Inicio de rango (≥ inclusivo)
to num / String Fin de rango (≤ inclusivo)
gt num / String Mayor que (> exclusivo)
lt num / String Menor que (< exclusivo)

Datos históricos: Los documentos escritos antes de registrar un campo como range no se re-indexan automáticamente. Llama a set() de nuevo sobre esos docs para que aparezcan en range queries.

Local Collections

Same API as any collection, but data stays on the device — no server sync. Useful when you need a local database that works alongside your cloud collections.

final students = app.collection('_students', local: true);

// Same API as any collection
await students.doc('s1').set({'name': 'Ana', 'age': 12, 'grade': '6A'});
final student = await students.doc('s1').get();
await students.doc('s1').delete();

await students.bulkSet({
  's1': {'name': 'Ana', 'age': 12},
  's2': {'name': 'Luis', 'age': 13},
});

A collection name cannot be both local and remote simultaneously.

Encryption

AES-256-GCM encryption, two modes:

// Per-collection: only sensitive collections are encrypted
final session = app.collection('_session', local: true, encrypted: true);
await session.doc('current').set({'jwt': token, 'email': 'user@test.com'});

// Global: ALL collections are encrypted
final app = await Aquabase.init(
  projectId: 'your_project_id',
  apiKey: 'aq_pub_...',
  encrypted: true,
);

When encrypted: true is set globally, every collection is encrypted automatically — no need to set encrypted on each one.

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();

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
collection(name, encrypted: true) Get an encrypted collection (AES-256-GCM)
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
encrypted bool false Encrypt ALL collections (AES-256-GCM)
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