Guides
Custom Storage
Implement AuthStorage against Redis, an encrypted file, or any backend you own.
SecureAuthStorage covers Flutter. For a Dart backend, CLI tool, or any runtime that isn't Flutter, implement AuthStorage yourself against whatever key-value store you already run.
The interface
abstract class AuthStorage {
Future<void> initialize();
Future<String?> read(String key);
Future<void> write(String key, String value);
Future<bool> delete(String key);
Future<void> clear();
Future<bool> containsKey(String key);
Future<List<String>> getKeysWithPrefix(String prefix);
}
SessionManager calls getKeysWithPrefix('session:') at startup to enumerate every persisted account, so your implementation must return the complete matching set, not a page of it.
Redis example
import 'package:authyra/authyra.dart';
import 'package:resp_client/resp_client.dart';
class RedisAuthStorage implements AuthStorage {
final RespClient _client;
final String _namespace;
RedisAuthStorage(this._client, {String namespace = 'authyra'})
: _namespace = namespace;
String _k(String key) => '$_namespace:$key';
@override
Future<void> initialize() async {
await _client.command(['PING']); // fail fast if Redis is unreachable
}
@override
Future<String?> read(String key) async {
final result = await _client.command(['GET', _k(key)]);
return result.payload as String?;
}
@override
Future<void> write(String key, String value) async {
await _client.command(['SET', _k(key), value]);
}
@override
Future<bool> delete(String key) async {
final deleted = await _client.command(['DEL', _k(key)]);
return (deleted.payload as int) > 0;
}
@override
Future<void> clear() async {
final keys = await getKeysWithPrefix('');
if (keys.isNotEmpty) {
await _client.command(['DEL', ...keys.map(_k)]);
}
}
@override
Future<bool> containsKey(String key) async {
final result = await _client.command(['EXISTS', _k(key)]);
return (result.payload as int) > 0;
}
@override
Future<List<String>> getKeysWithPrefix(String prefix) async {
final result = await _client.command(['KEYS', '${_k(prefix)}*']);
final fullKeys = (result.payload as List).cast<String>();
return fullKeys.map((k) => k.substring('$_namespace:'.length)).toList();
}
}
final client = AuthyraClient(
providers: [myProvider],
storage: RedisAuthStorage(redisClient),
);
await client.initialize();
Encrypted file example (CLI / desktop)
import 'dart:convert';
import 'dart:io';
import 'package:authyra/authyra.dart';
import 'package:encrypt/encrypt.dart';
class EncryptedFileStorage implements AuthStorage {
final File _file;
final Encrypter _encrypter;
final IV _iv;
Map<String, String> _cache = {};
EncryptedFileStorage(this._file, this._encrypter, this._iv);
@override
Future<void> initialize() async {
if (!await _file.exists()) {
_cache = {};
return;
}
final encrypted = await _file.readAsString();
final decrypted = _encrypter.decrypt64(encrypted, iv: _iv);
_cache = Map<String, String>.from(jsonDecode(decrypted) as Map);
}
Future<void> _persist() async {
final encrypted = _encrypter.encrypt(jsonEncode(_cache), iv: _iv);
await _file.writeAsString(encrypted.base64);
}
@override
Future<String?> read(String key) async => _cache[key];
@override
Future<void> write(String key, String value) async {
_cache[key] = value;
await _persist();
}
@override
Future<bool> delete(String key) async {
final existed = _cache.remove(key) != null;
if (existed) await _persist();
return existed;
}
@override
Future<void> clear() async {
_cache.clear();
await _persist();
}
@override
Future<bool> containsKey(String key) async => _cache.containsKey(key);
@override
Future<List<String>> getKeysWithPrefix(String prefix) async =>
_cache.keys.where((k) => k.startsWith(prefix)).toList();
}
Never persist tokens in plaintext. The Redis example relies on Redis's own at-rest encryption/transport security; the file example encrypts explicitly. Pick whichever guarantee fits your deployment, but don't skip it.
Testing your implementation
Reuse the same contract tests you'd run against any AuthStorage:
test('round-trips a value', () async {
final storage = RedisAuthStorage(testClient);
await storage.initialize();
await storage.write('k', 'v');
expect(await storage.read('k'), 'v');
expect(await storage.delete('k'), isTrue);
expect(await storage.read('k'), isNull);
});
test('getKeysWithPrefix returns every matching key', () async {
final storage = RedisAuthStorage(testClient);
await storage.initialize();
await storage.write('session:a', '{}');
await storage.write('session:b', '{}');
await storage.write('other:c', '{}');
expect(await storage.getKeysWithPrefix('session:'), unorderedEquals(['session:a', 'session:b']));
});