diff --git a/lib/matrix.dart b/lib/matrix.dart
index bbd34d5a..374d9319 100644
--- a/lib/matrix.dart
+++ b/lib/matrix.dart
@@ -24,6 +24,7 @@ export 'package:matrix_api_lite/matrix_api_lite.dart';
export 'src/client.dart';
export 'src/database/database_api.dart';
export 'src/database/hive_database.dart';
+export 'src/database/sembast_database.dart';
export 'src/event.dart';
export 'src/event_status.dart';
export 'src/room.dart';
diff --git a/lib/src/database/sembast_database.dart b/lib/src/database/sembast_database.dart
new file mode 100644
index 00000000..c11b2c4a
--- /dev/null
+++ b/lib/src/database/sembast_database.dart
@@ -0,0 +1,1528 @@
+/*
+ * Famedly Matrix SDK
+ * Copyright (C) 2019, 2020, 2021 Famedly GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:math';
+import 'dart:typed_data';
+
+import 'package:sembast/sembast.dart';
+import 'package:matrix/encryption/utils/olm_session.dart';
+import 'package:matrix/encryption/utils/outbound_group_session.dart';
+import 'package:matrix/encryption/utils/ssss_cache.dart';
+import 'package:matrix/encryption/utils/stored_inbound_group_session.dart';
+import 'package:matrix/matrix.dart' hide Filter;
+import 'package:matrix/src/event_status.dart';
+import 'package:matrix/src/utils/queued_to_device_event.dart';
+import 'package:matrix/src/utils/run_benchmarked.dart';
+import 'package:sembast/sembast_memory.dart';
+import 'package:sembast/utils/value_utils.dart';
+
+/// Sembast implementation of the DatabaseAPI. You need to pass through the
+/// correct dbfactory. By default it uses an in-memory database so there is no
+/// persistent storage. Learn more on: https://pub.dev/packages/sembast
+class MatrixSembastDatabase extends DatabaseApi {
+ static const int version = 5;
+ final String name;
+ final String path;
+ late final Database _database;
+ Transaction? _currentTransaction;
+
+ /// The transaction to use here. If there is a real transaction ongoing it
+ /// will use it and otherwise just use the default which is the database
+ /// object itself.
+ DatabaseClient get txn => (_transactionLock?.isCompleted ?? true)
+ ? _database
+ : _currentTransaction ?? _database;
+
+ final DatabaseFactory _dbFactory;
+
+ late final StoreRef _clientBox = StoreRef(_clientBoxName);
+ late final StoreRef> _accountDataBox =
+ StoreRef(_accountDataBoxName);
+ late final StoreRef> _roomsBox =
+ StoreRef(_roomsBoxName);
+ late final StoreRef> _toDeviceQueueBox =
+ StoreRef(_toDeviceQueueBoxName);
+
+ /// Key is a tuple as SembastKey(roomId, type) where stateKey can be
+ /// an empty string.
+ late final StoreRef> _roomStateBox =
+ StoreRef(_roomStateBoxName);
+
+ /// Key is a tuple as SembastKey(roomId, userId)
+ late final StoreRef> _roomMembersBox =
+ StoreRef(_roomMembersBoxName);
+
+ /// Key is a tuple as SembastKey(roomId, type)
+ late final StoreRef> _roomAccountDataBox =
+ StoreRef(_roomAccountDataBoxName);
+ late final StoreRef> _inboundGroupSessionsBox =
+ StoreRef(_inboundGroupSessionsBoxName);
+ late final StoreRef> _outboundGroupSessionsBox =
+ StoreRef(_outboundGroupSessionsBoxName);
+ late final StoreRef> _olmSessionsBox =
+ StoreRef(_olmSessionsBoxName);
+
+ /// Key is a tuple as SembastKey(userId, deviceId)
+ late final StoreRef> _userDeviceKeysBox =
+ StoreRef(_userDeviceKeysBoxName);
+
+ /// Key is the user ID as a String
+ late final StoreRef _userDeviceKeysOutdatedBox =
+ StoreRef(_userDeviceKeysOutdatedBoxName);
+
+ /// Key is a tuple as SembastKey(userId, publicKey)
+ late final StoreRef> _userCrossSigningKeysBox =
+ StoreRef(_userCrossSigningKeysBoxName);
+ late final StoreRef> _ssssCacheBox =
+ StoreRef(_ssssCacheBoxName);
+ late final StoreRef> _presencesBox =
+ StoreRef(_presencesBoxName);
+
+ /// Key is a tuple as Multikey(roomId, fragmentId) while the default
+ /// fragmentId is an empty String
+ late final StoreRef> _timelineFragmentsBox =
+ StoreRef(_timelineFragmentsBoxName);
+
+ /// Key is a tuple as SembastKey(roomId, eventId)
+ late final StoreRef> _eventsBox =
+ StoreRef(_eventsBoxName);
+
+ /// Key is a tuple as SembastKey(userId, deviceId)
+ late final StoreRef _seenDeviceIdsBox =
+ StoreRef(_seenDeviceIdsBoxName);
+
+ late final StoreRef _seenDeviceKeysBox =
+ StoreRef(_seenDeviceKeysBoxName);
+
+ String get _clientBoxName => '$name.box.client';
+
+ String get _accountDataBoxName => '$name.box.account_data';
+
+ String get _roomsBoxName => '$name.box.rooms';
+
+ String get _toDeviceQueueBoxName => '$name.box.to_device_queue';
+
+ String get _roomStateBoxName => '$name.box.room_states';
+
+ String get _roomMembersBoxName => '$name.box.room_members';
+
+ String get _roomAccountDataBoxName => '$name.box.room_account_data';
+
+ String get _inboundGroupSessionsBoxName => '$name.box.inbound_group_session';
+
+ String get _outboundGroupSessionsBoxName =>
+ '$name.box.outbound_group_session';
+
+ String get _olmSessionsBoxName => '$name.box.olm_session';
+
+ String get _userDeviceKeysBoxName => '$name.box.user_device_keys';
+
+ String get _userDeviceKeysOutdatedBoxName =>
+ '$name.box.user_device_keys_outdated';
+
+ String get _userCrossSigningKeysBoxName => '$name.box.cross_signing_keys';
+
+ String get _ssssCacheBoxName => '$name.box.ssss_cache';
+
+ String get _presencesBoxName => '$name.box.presences';
+
+ String get _timelineFragmentsBoxName => '$name.box.timeline_fragments';
+
+ String get _eventsBoxName => '$name.box.events';
+
+ String get _seenDeviceIdsBoxName => '$name.box.seen_device_ids';
+
+ String get _seenDeviceKeysBoxName => '$name.box.seen_device_keys';
+
+ final SembastCodec? codec;
+
+ MatrixSembastDatabase(
+ this.name, {
+ this.path = './database.db',
+ this.codec,
+ DatabaseFactory? dbFactory,
+ }) : _dbFactory = dbFactory ?? databaseFactoryMemory;
+
+ @override
+ int get maxFileSize => 0;
+
+ Future _actionOnAllBoxes(Future Function(StoreRef box) action) =>
+ Future.wait([
+ action(_clientBox),
+ action(_accountDataBox),
+ action(_roomsBox),
+ action(_roomStateBox),
+ action(_roomMembersBox),
+ action(_toDeviceQueueBox),
+ action(_roomAccountDataBox),
+ action(_inboundGroupSessionsBox),
+ action(_outboundGroupSessionsBox),
+ action(_olmSessionsBox),
+ action(_userDeviceKeysBox),
+ action(_userDeviceKeysOutdatedBox),
+ action(_userCrossSigningKeysBox),
+ action(_ssssCacheBox),
+ action(_presencesBox),
+ action(_timelineFragmentsBox),
+ action(_eventsBox),
+ action(_seenDeviceIdsBox),
+ action(_seenDeviceKeysBox),
+ ]);
+
+ Future open() async {
+ _database = await _dbFactory.openDatabase(path, codec: codec);
+
+ // Check version and check if we need a migration
+ final currentVersion =
+ (await _clientBox.record('version').get(txn) as int?);
+ if (currentVersion == null) {
+ await _clientBox.record('version').put(txn, version);
+ } else if (currentVersion != version) {
+ await _migrateFromVersion(currentVersion);
+ }
+
+ return;
+ }
+
+ Future _migrateFromVersion(int currentVersion) async {
+ Logs()
+ .i('Migrate Sembast database from version $currentVersion to $version');
+ if (version == 5) {
+ await _database.transaction((txn) async {
+ final keys = await _userDeviceKeysBox.findKeys(txn);
+ for (final key in keys) {
+ try {
+ final raw = await _userDeviceKeysBox.record(key).get(txn) as Map;
+ if (!raw.containsKey('keys')) continue;
+ final deviceKeys = DeviceKeys.fromJson(
+ cloneMap(raw),
+ Client(''),
+ );
+ await addSeenDeviceId(deviceKeys.userId, deviceKeys.deviceId!,
+ deviceKeys.curve25519Key! + deviceKeys.ed25519Key!);
+ await addSeenPublicKey(
+ deviceKeys.ed25519Key!, deviceKeys.deviceId!);
+ await addSeenPublicKey(
+ deviceKeys.curve25519Key!, deviceKeys.deviceId!);
+ } catch (e) {
+ Logs().w('Can not migrate device $key', e);
+ }
+ }
+ });
+ }
+ await clearCache();
+ await _clientBox.record('version').put(txn, version);
+ }
+
+ @override
+ Future clear() async {
+ Logs().i('Clear and close Sembast database...');
+ await _actionOnAllBoxes((box) => box.delete(txn));
+ return;
+ }
+
+ @override
+ Future clearCache() async {
+ await _roomsBox.delete(txn);
+ await _accountDataBox.delete(txn);
+ await _roomStateBox.delete(txn);
+ await _roomMembersBox.delete(txn);
+ await _eventsBox.delete(txn);
+ await _timelineFragmentsBox.delete(txn);
+ await _outboundGroupSessionsBox.delete(txn);
+ await _presencesBox.delete(txn);
+ await _clientBox.record('prev_batch').delete(txn);
+ }
+
+ @override
+ Future clearSSSSCache() async {
+ await _ssssCacheBox.delete(txn);
+ }
+
+ @override
+ Future close() async {
+ // We never close a sembast database
+ // https://github.com/tekartik/sembast.dart/issues/219
+ }
+
+ @override
+ Future deleteFromToDeviceQueue(int id) async {
+ await _toDeviceQueueBox.record(id).delete(txn);
+ return;
+ }
+
+ @override
+ Future deleteOldFiles(int savedAt) async {
+ return;
+ }
+
+ @override
+ Future forgetRoom(String roomId) async {
+ await _timelineFragmentsBox
+ .record(SembastKey(roomId, '').toString())
+ .delete(txn);
+ final eventKeys = await _eventsBox.findKeys(txn);
+ for (final key in eventKeys) {
+ final multiKey = SembastKey.fromString(key);
+ if (multiKey.parts.first != roomId) continue;
+ await _eventsBox.record(key).delete(txn);
+ }
+ final roomStateKeys = await _roomStateBox.findKeys(txn);
+ for (final key in roomStateKeys) {
+ final multiKey = SembastKey.fromString(key);
+ if (multiKey.parts.first != roomId) continue;
+ await _roomStateBox.record(key).delete(txn);
+ }
+ final roomMembersKeys = await _roomMembersBox.findKeys(txn);
+ for (final key in roomMembersKeys) {
+ final multiKey = SembastKey.fromString(key);
+ if (multiKey.parts.first != roomId) continue;
+ await _roomMembersBox.record(key).delete(txn);
+ }
+ final roomAccountData = await _roomAccountDataBox.findKeys(txn);
+ for (final key in roomAccountData) {
+ final multiKey = SembastKey.fromString(key);
+ if (multiKey.parts.first != roomId) continue;
+ await _roomAccountDataBox.record(key).delete(txn);
+ }
+ await _roomsBox.record(roomId).delete(txn);
+ }
+
+ @override
+ Future