just_storage 1.1.2
just_storage: ^1.1.2 copied to clipboard
Standard and secure key-value storage with typed helpers and reactive streams.
Just Storage #
A Flutter package providing standard and secure key-value storage with typed JSON helpers and reactive watch streams — via JustStandardStorage and JustSecureStorage.
Built from scratch with no third-party storage wrappers (shared_preferences, flutter_secure_storage, etc.). On native platforms (Android, iOS, macOS, Linux, Windows) storage is backed by dart:io files; on web it uses the browser's localStorage — the same JustStorage factory selects the right backend automatically.
Contributions are welcome! Please read the Contributing Guide and Code of Conduct before opening an issue or pull request.
Features #
JustStandardStorage |
JustSecureStorage |
|
|---|---|---|
| Use case | Non-sensitive app data (settings, preferences, game state) | Sensitive data (auth tokens, credentials, PII) |
| Native backend | Atomic JSON file via dart:io |
AES-256-GCM encrypted JSON file |
| Web backend | window.localStorage (prefixed keys) |
window.localStorage with AES-256-GCM |
| Encryption | None | AES-256-GCM with per-value random nonces |
| Integrity check | No | Yes — GCM auth tag rejects any tampered byte |
| Persistence | ✅ | ✅ |
| Typed JSON helpers | ✅ | ✅ |
Reactive watch() stream |
✅ | ✅ |
| Atomic writes | ✅ native (rename-swap) / ✅ web (sync) | ✅ native (rename-swap) / ✅ web (sync) |
| Admin UI | ✅ via package:just_storage/ui.dart |
✅ via package:just_storage/ui.dart |
When to choose just_storage #
- You want both standard and secure storage under a single, uniform API.
- You need WASM / Flutter Web support for encrypted storage.
- You want authenticated encryption (AES-256-GCM) that detects file tampering — not just confidentiality.
- You want a built-in admin UI to inspect storage during development without extra tooling.
- You prefer zero code generation — no adapters, no
build_runner. - You want reactive streams on individual keys without pulling in a full reactive state library.
When to choose something else #
- You only need simple non-sensitive persistence and already use
shared_preferencesthroughout your codebase — no reason to migrate. - You rely on OS-level secure enclave / Keychain / Keystore hardware backing — use
flutter_secure_storage. - You store large datasets with complex queries — use a database (just_database, Isar, Drift, sqflite) rather than a key-value store.
- You need Hive's binary TypeAdapter format for performance-critical large objects.
Comparison with similar packages #
| just_storage | shared_preferences |
flutter_secure_storage |
hive |
|
|---|---|---|---|---|
| Non-sensitive storage | ✅ JustStandardStorage |
✅ | ❌ | ✅ |
| Encrypted storage | ✅ JustSecureStorage |
❌ | ✅ | ✅ (encrypted box) |
| Encryption algorithm | AES-256-GCM | — | OS keychain / keystore | AES-256-CBC |
| Authenticated encryption (tamper detection) | ✅ GCM auth tag | — | Platform-dependent | ❌ |
| Web support | ✅ WASM-compatible | ✅ | ⚠️ unofficial / limited | ✅ |
Reactive watch() stream |
✅ | ❌ | ❌ | ✅ (Box.watch) |
| Typed JSON helpers | ✅ built-in | ❌ | ❌ | ✅ via TypeAdapters |
| Code generation required | ❌ | ❌ | ❌ | ⚠️ for typed boxes |
| Atomic writes | ✅ rename-swap | ✅ platform-native | ✅ platform-native | ✅ |
| Built-in admin UI | ✅ JUStorageAdminScreen |
❌ | ❌ | ❌ |
| Third-party storage wrappers | ❌ none | SQLite / NSUserDefaults / SharedPreferences | OS keychain / keystore | Custom binary format |
| Dependencies | path_provider, pointycastle, web |
shared_preferences platform plugins |
flutter_secure_storage platform plugins |
hive, path_provider |
Getting started #
Add the package to your app's pubspec.yaml:
dependencies:
just_storage: ^1.1.0
Use [JustStorage] to obtain instances — no path_provider import required in your app:
import 'package:just_storage/just_storage.dart';
// Standard storage
final JustStandardStorage storage = await JustStorage.standard();
// Secure storage
final JustSecureStorage secure = await JustStorage.encrypted();
Register both stores in your DI container (e.g. get_it):
final standard = await JustStorage.standard();
final secure = await JustStorage.encrypted();
sl.registerLazySingleton<JustStandardStorage>(() => standard);
sl.registerLazySingleton<JustSecureStorage>(() => secure);
Usage #
Standard storage (JustStandardStorage) #
final JustStandardStorage storage = await JustStorage.standard();
// Read / write
await storage.write('theme', 'dark');
final theme = await storage.read('theme'); // 'dark'
// Typed JSON
await storage.writeJson('profile', profile, (p) => p.toJson());
final profile = await storage.readJson('profile', Profile.fromJson);
// Reactive watch — emits current value then every subsequent change
storage.watch('theme').listen((value) {
print('theme changed to: $value');
});
// Read all
final all = await storage.readAll();
// Delete / clear
await storage.delete('theme');
await storage.clear();
Secure storage (JustSecureStorage) #
final JustSecureStorage secure = await JustStorage.encrypted();
// Read / write (values are AES-256-GCM encrypted on disk)
await secure.write('access_token', token);
final token = await secure.read('access_token');
// Typed JSON
await secure.writeJson('user_credentials', creds, (c) => c.toJson());
final creds = await secure.readJson('user_credentials', Credentials.fromJson);
// Bulk operations
final all = await secure.readAll();
await secure.clear();
// Watch
secure.watch('access_token').listen((value) {
if (value == null) print('logged out');
});
Both types share the same method surface — the only difference is what happens on disk (plain JSON vs. encrypted JSON).
Admin UI #
just_storage ships a built-in Flutter admin screen for inspecting and editing storage at runtime. Import it from the separate ui.dart library to keep the UI dependencies optional:
import 'package:just_storage/ui.dart';
// Push the screen (creates its own storage instances automatically)
Navigator.of(context).push(MaterialPageRoute(
builder: (_) => const JUStorageAdminScreen(),
));
Pass your own instances to inspect a specific directory or localStorage namespace:
JUStorageAdminScreen(
standard: myStandardStorage,
secure: mySecureStorage,
)
Optionally supply a custom theme:
JUStorageAdminScreen(
theme: ThemeData(colorSchemeSeed: Colors.teal),
)
The screen contains three tabs:
| Tab | Description |
|---|---|
| Standard | Browse, search, add, edit, and delete plain key-value entries |
| Secure | Same operations for AES-256-GCM encrypted entries |
| Info | Entry counts, backend details, and danger-zone clear actions |
Security model #
Native platforms (Android, iOS, macOS, Linux, Windows) #
Encryption
EncryptedFileStorage uses AES-256-GCM (authenticated encryption) via the pointycastle package:
- Algorithm: AES-256-GCM
- Key length: 256 bits (32 bytes), generated once with
Random.secure() - Nonce: 96 bits (12 bytes), freshly generated per
writecall — nonce reuse is impossible - Auth tag: 128 bits appended to every ciphertext — any modification to the file throws
StorageExceptionbefore any plaintext is returned - Each value independently encrypted: compromising one entry's nonce does not affect others
Master key
The master key is stored in <appSupportDir>/.storage.key:
- Generated once with
Random.secure()and flushed withFile.writeAsBytes(flush: true) - File permissions set to
0600(owner-read-only) on POSIX platforms (Android, iOS, Linux, macOS) - Protected by the OS app sandbox — inaccessible to other apps on Android (API 29+) and iOS without root
On-disk format
{
"access_token": { "n": "<base64 nonce>", "ct": "<base64 ciphertext+GCMtag>" },
"refresh_token": { "n": "<base64 nonce>", "ct": "<base64 ciphertext+GCMtag>" }
}
Atomic writes
Both implementations write to a .tmp file first, then call File.rename() — an atomic OS-level swap on all supported platforms. A crash mid-write never corrupts the existing store.
Web platform #
WebSecureStorage uses the same AES-256-GCM algorithm via pointycastle. The master key is generated on first use with Random.secure() and stored in localStorage under a reserved key.
Note:
localStorageis accessible to all scripts running on the same origin. The security guarantee is equivalent to in-memory protection rather than OS-level sandboxing. Prefer a server-side solution for highly sensitive data in web applications.
Error handling #
All operations throw StorageException on failure:
try {
await secure.read('token');
} on StorageException catch (e) {
print(e.message); // human-readable description
print(e.cause); // underlying exception, if any
}
A StorageException is also thrown if:
- the encrypted file was modified externally (GCM tag mismatch)
- the master key file is corrupt (wrong byte count)
Testing #
Both implementations accept a Directory via constructor, so tests simply pass Directory.systemTemp.createTempSync():
final dir = Directory.systemTemp.createTempSync('test_');
final storage = FileStorage(dir); // or EncryptedFileStorage(dir)
addTearDown(() => dir.deleteSync(recursive: true));