ReaxDB
A database for Flutter and Dart that does not lose your data, with queries, transactions and encryption. Pure Dart, no native code, no code generation.
Start in 30 seconds
In a Flutter app
flutter pub add reaxdb_flutter
import 'package:reaxdb_flutter/reaxdb_flutter.dart';
final db = await ReaxFlutter.open('app');
await db.put('user:1', {'name': 'Ada', 'age': 36});
final user = await db.get<Map<String, dynamic>>('user:1');
That is all. reaxdb_flutter
puts the database in the app's private folder. Pass encrypted: true and it
also creates an encryption key and keeps it in the Keychain or Keystore.
In Dart (server, command line)
dart pub add reaxdb_dart
import 'package:reaxdb_dart/reaxdb_dart.dart';
final db = await ReaxDB.open(path: '/absolute/path/to/data');
Use an absolute path. In Flutter without reaxdb_flutter, get one from
path_provider: on iOS and Android
a relative path points to a read-only folder, and open fails with a
StorageException.
Needs Dart 3.12 or later (Flutter 3.44 or later). Runs on Android, iOS, macOS, Windows, Linux and the Dart VM. It does not run on the web.
What you get
- Your data is really saved. Every write goes to a log on disk and is
flushed before
putreturns. A crash or a power cut does not lose it (with one limit on Apple devices). - Queries. Filter, sort and page documents, with indexes to make it fast.
- Transactions. Change several keys at once: all of the changes are saved, or none.
- Encryption. AES-256-GCM, which also detects tampering.
- Live updates. Watch a key, a prefix or a whole query and rebuild your UI when it changes.
- Expiry, backups and migrations. Entries can expire, a database can be exported to one file and restored, and a schema version runs your migration code.
ReaxDB or Hive?
Both are pure Dart. This table compares documented features, with
hive_ce 2.22.0 (October 2026).
| ReaxDB | Hive (hive_ce) |
|
|---|---|---|
| Each write flushed to disk before it returns | Yes, by default | No, call flush() |
| Queries and indexes | Yes | No, key/value only |
| Transactions over several keys | Yes | Not in the documented API |
| Encryption | AES-256-GCM (authenticated) | AES-256-CBC |
| Entries that expire (TTL) | Yes | No |
| Typed objects | toJson/fromJson, no code generation |
TypeAdapters, usually generated |
| Reads | Cached, data on disk; or every value in memory with keepInMemory: true (2.5.0) |
Normal boxes keep every value in memory |
| Background isolate | Yes, background: true (2.6.0) |
Yes (IsolatedHive) |
| Web | No | Yes (IndexedDB) |
Speed, measured on the same machine with 100,000 documents (details and smaller sizes in BENCHMARKS.md):
| ReaxDB | ReaxDB, keepInMemory |
Hive | |
|---|---|---|---|
| Durable write | 19.0k/s | 19.2k/s | 23.5k/s (put + flush) |
| Batch of 500 | 199.5k/s | 309.1k/s | |
| Read | 44.3k/s | 0.99M/s | 0.78M/s |
| Open | 11 ms | 177 ms | 261 ms |
| Query, 20,000 results | 449 ms | 24 ms | 3.9 ms (values.where) |
Choose Hive if you need the web, or the fastest possible iteration over everything you store. Choose ReaxDB if losing a write is not acceptable, or if you need queries, transactions or expiry.
Common tasks
Save, read and delete
await db.put('settings:theme', 'dark');
final theme = await db.get<String>('settings:theme');
await db.putBatch({'a': 1, 'b': 2}); // one atomic write
await db.delete('settings:theme');
final there = await db.exists('settings:theme'); // false
Keys are strings. Values can be strings, numbers, booleans, null, bytes, or maps and lists of those.
Go through many entries
await for (final entry in db.scanPrefix<Map<String, dynamic>>('users:')) {
print('${entry.key}: ${entry.value}');
}
final firstPage = await db.range<int>('a', 'm', limit: 50);
Documents and queries
A document is a map stored under collection:id.
await db.put('users:1', {'name': 'Ada', 'city': 'London', 'age': 36});
await db.put('users:2', {'name': 'Alan', 'city': 'London', 'age': 41});
await db.createIndex('users', ['city']); // optional, makes the query fast
final londoners = await db.query('users')
.whereEquals('city', 'London')
.orderBy('age', descending: true)
.limit(20)
.find();
Typed objects
No code generation: give ReaxDB your fromJson and toJson.
class User {
User({required this.name, required this.age});
factory User.fromJson(Map<String, dynamic> json) =>
User(name: json['name'] as String, age: json['age'] as int);
final String name;
final int age;
Map<String, dynamic> toJson() => {'name': name, 'age': age};
}
final users = db.collection<User>(
'users',
fromJson: User.fromJson,
toJson: (user) => user.toJson(),
);
await users.put('1', User(name: 'Ada', age: 36));
final ada = await users.get('1');
final adults = await users.where('age', QueryOperator.greaterThan, 18);
Rebuild the UI when data changes
db.watch('users:*').listen((event) => print('${event.type} ${event.key}'));
With reaxdb_flutter, a query becomes a stream for StreamBuilder. It runs
again whenever the collection changes:
final stream = db.watchQuery(db.query('users').whereEquals('city', 'London'));
Transactions
await db.transaction((tx) async {
final from = await tx.get<Map<String, dynamic>>('account:1');
final to = await tx.get<Map<String, dynamic>>('account:2');
await tx.put('account:1', {...from!, 'balance': from['balance'] - 50});
await tx.put('account:2', {...to!, 'balance': to['balance'] + 50});
}, isolationLevel: IsolationLevel.serializable);
Use IsolationLevel.serializable when you read a value and write it back,
as above. The default level is faster but can lose one of two concurrent
updates. ADVANCED.md explains every level.
Encryption
In Flutter, ReaxFlutter.open('app', encrypted: true) handles the key for
you. In Dart, pass a 32-byte key or a passphrase:
final db = await ReaxDB.open(
path: '/absolute/path/to/data',
encryption: EncryptionConfig.aes256(key: key32Bytes),
// or: EncryptionConfig.aes256FromPassphrase(passphrase: secret),
);
Values are encrypted. Keys and indexed fields are not, so do not put secrets
in keys and do not index a secret field. A wrong key fails at open.
To change the key (2.5.0), call rekey. It returns the database opened with
the new key:
db = await db.rekey(EncryptionConfig.aes256(key: newKey32Bytes));
// or: db = await db.changePassphrase('new secret');
Entries that expire
await db.put('session:abc', token, ttl: const Duration(minutes: 30));
Backup and restore
await db.exportTo('/backups/app.rxdb');
final restored = await ReaxDB.importFrom(
archivePath: '/backups/app.rxdb',
path: '/absolute/path/to/restored',
);
The backup file is not encrypted unless you pass encryption: to
exportTo (and the same to importFrom as archiveEncryption:).
Schema migrations
final db = await ReaxDB.open(
path: '/absolute/path/to/data',
schemaVersion: 2,
onUpgrade: (from, to, db) async {
// Update your documents from version `from` to `to` here.
},
);
Keep everything in memory (2.5.0)
For data that fits in memory and is read far more than it is written, keep every value decoded in memory, like a Hive box:
final db = await ReaxDB.open(path: '/absolute/path/to/data', keepInMemory: true);
final theme = db.getSync<String>('settings:theme'); // no await, e.g. in build()
Reads and queries then never touch the disk. Writes are still saved to disk first, exactly as before. Opening takes longer, because it loads everything. See ADVANCED.md.
Keep the UI isolate free (2.6.0)
final db = await ReaxDB.open(
path: '/absolute/path/to/data',
background: true,
keepInMemory: true, // optional; getSync keeps working
);
The engine (write-ahead log, flushes and compactions, encryption, indexes) then runs in an isolate of its own, so a long compaction never freezes the UI. The API is the same; each call costs a few microseconds more. See ADVANCED.md.
Faster writes when you can afford to lose the last ones
final db = await ReaxDB.open(path: '/absolute/path/to/cache', syncMode: SyncMode.os);
SyncMode.full (the default) survives power loss, SyncMode.os survives the
app being killed, and SyncMode.none is for throwaway data. Writing many
values with putBatch is fast in every mode.
Good to know
- One instance per database. Opening a database that is already open,
in this or another process, throws
DatabaseLockedException. Close it withawait db.close(). - Errors are typed. Every failure is a
ReaxDbException, never a silentnull. - Upgrading from 1.x? Your data migrates, encrypted data too. See MIGRATION.md.
Documentation
- ADVANCED.md: durability, isolation levels, encryption details, tuning, errors and internals
- BENCHMARKS.md: measured performance and how it was measured
- MIGRATION.md: moving from 1.x
- CHANGELOG.md
Contributing
Issues and pull requests are welcome at github.com/dvxlabs520/Reax-BD. A bug fix should come with a test that fails before it and passes after.
License
MIT. See LICENSE.
Libraries
- reaxdb_dart
- ReaxDB - an embedded NoSQL database written in pure Dart.
- reaxdb_dart_io
dart:io-dependent additions to ReaxDB.