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']));
});
Copyright © 2026