Skip to content
Back to archipelago.banuacoder.com

Infrastructure SDKs

storage_sdk Free ​

Tier
Freeversion 3.1.4
Command
archipelago generate storage_sdk
Generates
  • infrastructure/storage_sdk_api
  • infrastructure/storage_sdk_shared_preferences
Generates first
test_prereqwhen needed and missing, so there is nothing to install first

Local key-value storage abstraction (StorageProvider/AppStorage) with three independent axes — namespace, secure, and scope — backed by SharedPreferencesAsync and FlutterSecureStorage, registered once, globally, via @InjectableInit.microPackage().

Variables ​

VariableTypeDefaultDescription
androidPreferencesBackendenum (dataStore, sharedPreferences)dataStoreAndroid backend for non-secure storage
iosKeychainAccessibilityenum (passcode, unlocked, unlocked_this_device, first_unlock, first_unlock_this_device)first_unlockiOS/macOS Keychain accessibility level for secure storage
iosUseSecureEnclavebooleantrueUse Secure Enclave-backed keys for secure storage on iOS/macOS

Why this brick exists ​

Several feature bricks (biometric_auth_sdk, feedback_sdk, rating_review_sdk, ai_chat_sdk, auth_sdk) need local persistence, some of it secret (auth tokens) and some of it not. Each registering its own storage instance in the app's global GetIt container crashes at boot the moment two of them are installed together — GetIt throws on duplicate type registration. storage_sdk centralizes registration behind one type (StorageProvider), registered exactly once, so the collision is structurally impossible instead of order-dependent.

This brick also unifies what used to be two separate SDKs — storage_sdk (namespaced, non-secure only) and secure_storage_sdk (secure, flat only, retired) — behind one contract, so a feature choosing secure: true doesn't need a second SDK with a completely different, non-namespaced, non-scoped API just because it flipped one axis.

The three axes ​

StorageProvider.forNamespace takes three independent axes:

  • namespace (String) — isolates one feature's keys from every other's. Isolation is key-prefix based ("$namespace::$key") within one shared backend instance per axis, not by separate OS-level files — Android's per-file option only works outside the modern DataStore backend, and iOS's per-suite option requires a manually provisioned App Group entitlement no generator can automate.
  • secure (bool, default false) — true routes through FlutterSecureStorage (Keychain on iOS, with Secure Enclave-backed keys by default; Keystore-backed encryption on Android); false routes through SharedPreferencesAsync (DataStore backend by default). Never changes based on scope.
  • scope (StorageScope, default application) — StorageScope.user registers the namespace to be swept by clearAllUserScoped(), called once by a central logout-clear coordinator (never by individual features). StorageScope.application namespaces are never auto-cleared. Pure bookkeeping — never changes which backend handles the namespace.
dart
// Non-secret, survives logout (e.g. onboarding-seen flag).
final settings = storageProvider.forNamespace('settings');

// Secret, cleared automatically on logout (e.g. auth tokens).
final tokens = storageProvider.forNamespace(
  'auth',
  secure: true,
  scope: StorageScope.user,
);

Per-namespace secure-storage hardening ​

secure: true alone gives every namespace the same app-wide protection level (iosKeychainAccessibility + iosUseSecureEnclave, configured once at generation time). That's the right default for short-lived bearer tokens, but it's not enough for a namespace holding long-lived encryption-key material — e.g. a SQLCipher/local-database encryption key — which needs to be device-bound and excluded from backups: restoring an encrypted device backup onto a different device should never hand that device a working key.

Pass hardening (a SecureStorageHardening, from storage_sdk_api) to request stronger or different protection for one namespace, without changing anything about the app-wide default or any other secure: true namespace:

dart
final databaseKey = storageProvider.forNamespace(
  'database-dek',
  secure: true,
  scope: StorageScope.application,
  hardening: const SecureStorageHardening(
    accessibility: SecureAccessibility.firstUnlockThisDevice,
    androidStorageNamespace: 'database_dek',
  ),
);
  • accessibility (SecureAccessibility) — vendor-neutral mirror of the iOS/macOS Keychain accessibility axis (passcode, unlocked, unlockedThisDevice, firstUnlock, firstUnlockThisDevice), mapped onto flutter_secure_storage's KeychainAccessibility for both iOptions and mOptions. storage_sdk_api never depends on flutter_secure_storage, so this enum has its own (camelCase) member names rather than reusing the vendor's.
  • androidStorageNamespace (String?) — a distinct Android Keystore storage namespace, isolating this entry's wrapping key/alias from every other secure namespace that doesn't request one. null uses the app-wide default (no storageNamespace, i.e. flutter_secure_storage's own default).

Every namespace requesting the same hardening value (by ==, not identity) shares one underlying FlutterSecureStorage instance — hardening: null (the default) always resolves to the same app-wide instance every unhardened secure: true namespace already used, so nothing changes for callers that don't opt in. hardening is only meaningful when secure: true; passing it with secure: false, or requesting the same namespace with two different non-null hardening values across calls, fails an assertion instead of silently doing the wrong thing.

Logout-clear coordination ​

clearAllUserScoped() sweeps every scope: user namespace across every installed feature, regardless of secure. It's wired up by a small coordinating Initializer that lives in the generated app layer — it resolves both AuthSDK and StorageProvider from the service locator and listens for AuthLogoutEvent. Neither auth_impl nor storage_sdk_shared_preferences ever imports the other; avoiding that direct dependency between two independent SDKs is deliberate. See auth_sdk's _AuthStorageClearInitializer for the reference implementation.

Usage ​

Interactive ​

bash
archipelago generate storage_sdk

Non-interactive (CI) ​

bash
archipelago generate storage_sdk --config my_config.json

Config Template ​

json
{
  "androidPreferencesBackend": "dataStore",
  "iosKeychainAccessibility": "first_unlock",
  "iosUseSecureEnclave": true
}

Most consumers never need to generate this brick manually — any feature brick with a hard dependency on storage_sdk self-generates it if it isn't already installed.

Generated Structure ​

text
infrastructure/
├── storage_sdk_api/
│   ├── lib/
│   │   ├── storage_sdk_api.dart
│   │   └── src/
│   │       ├── app_storage.dart                     # AppStorage abstract interface
│   │       ├── secure_storage_hardening.dart        # SecureStorageHardening + SecureAccessibility
│   │       └── storage_provider.dart                # StorageProvider + StorageScope
│   ├── pubspec.yaml
│   └── README.md
└── storage_sdk_shared_preferences/
    ├── lib/
    │   ├── storage_sdk_shared_preferences.dart
    │   ├── testing.dart                         # test-only entry point
    │   └── src/
    │       ├── shared_preferences_app_storage.dart  # non-secure AppStorage
    │       ├── secure_app_storage.dart              # secure AppStorage
    │       ├── shared_preferences_storage_provider.dart
    │       ├── testing/                         # StorageSdkSharedPreferencesTestPrerequisite
    │       └── di/
    │           ├── injector.dart
    │           ├── injector.module.dart
    │           └── register_module.dart
    ├── test/
    ├── pubspec.yaml
    └── README.md

API ​

dart
enum StorageScope { user, application }

enum SecureAccessibility {
  passcode,
  unlocked,
  unlockedThisDevice,
  firstUnlock,
  firstUnlockThisDevice,
}

class SecureStorageHardening {
  const SecureStorageHardening({
    required this.accessibility,
    this.androidStorageNamespace,
  });

  final SecureAccessibility accessibility;
  final String? androidStorageNamespace;
}

abstract interface class StorageProvider {
  AppStorage forNamespace(
    String namespace, {
    bool secure = false,
    StorageScope scope = StorageScope.application,
    SecureStorageHardening? hardening,
  });

  Future<void> clearAllUserScoped();
}

abstract interface class AppStorage {
  Future<String?> getString(String key);
  Future<void> setString(String key, String value);
  Future<bool?> getBool(String key);
  Future<void> setBool(String key, bool value);
  Future<int?> getInt(String key);
  Future<void> setInt(String key, int value);
  Future<double?> getDouble(String key);
  Future<void> setDouble(String key, double value);
  Future<List<String>?> getStringList(String key);
  Future<void> setStringList(String key, List<String> value);
  Future<void> remove(String key);
  Future<void> clear();
  Future<Set<String>> getKeys();
}

Key Features ​

  • Zero manual wiring — StorageSdkSharedPreferencesPackageModule registers StorageProvider as a global @lazySingleton, discovered automatically by monorepo_toolkit generate host-registry the same way every other infrastructure SDK is
  • Standalone-generatable — any consumer brick self-generates storage_sdk if it isn't already installed, so archipelago generate auth_sdk (or any other consumer) works into a bare workspace with no separate manual prerequisite step
  • Secure Enclave by default — iosUseSecureEnclave defaults to true; gracefully falls back to standard Keychain storage on devices/simulators without a Secure Enclave
  • Per-namespace hardening — hardening: SecureStorageHardening(...) lets one secure: true namespace opt into stronger, device-bound, non-backed-up protection (and its own Android Keystore namespace) for long-lived key material, without touching the app-wide default
  • Vendor-agnostic contract — storage_sdk_api has zero external dependencies (only meta), so feature packages can depend on it without pulling in shared_preferences/flutter_secure_storage transitively

Testing ​

SharedPreferencesStorageProvider builds SharedPreferencesAsync in its initializer list. That constructor throws when no platform is registered, which is the normal state under plain flutter test. ai_chat_sdk's global module also reads a non-secure namespace during initializeInjector(), so the composition root fails without a fake.

The package ships the fake as a TestPrerequisite (see test_prereq) and declares it in its own pubspec.yaml:

yaml
archipelago:
  test_prerequisites:
    - class: StorageSdkSharedPreferencesTestPrerequisite
      reason: >-
        SharedPreferencesStorageProvider's initializer list constructs
        SharedPreferencesAsync, which throws with no platform registered;
        AiChatImplGlobalModule.init() resolves StorageProvider and reads it
        during initializeInjector().

The composition-root harness generator reads this declaration. Until that harness ships, install it yourself:

dart
import 'package:storage_sdk_shared_preferences/testing.dart';

setUpAll(() async {
  await StorageSdkSharedPreferencesTestPrerequisite().install();
  await initializeInjector();
});

install() sets SharedPreferencesAsyncPlatform.instance to an empty in-memory store, once per process. A second call does nothing, so data that earlier tests wrote stays. It does not fake FlutterSecureStorage: a test that uses secure: true namespaces still needs its own fake. Import testing.dart only from test code.

Not the same scope as a local database ​

storage_sdk is preference/key-value storage only — namespace isolation, optional encryption, optional logout-scoping. It is explicitly not a replacement for a full local database (Hive, Isar, sqflite/Drift, etc.); those have different lifecycle and query requirements and are out of scope for this brick.

Dependencies ​

  • storage_sdk_shared_preferences depends on shared_preferences: ^2.5.5, shared_preferences_android (for the androidPreferencesBackend knob), flutter_secure_storage: ^10.3.1, meta, shared_preferences_platform_interface (for the test prerequisite's in-memory store), and test_prereq. pre_gen self-generates test_prereq if utilities/test_prereq is missing
  • storage_sdk_api has no external dependencies beyond meta

Migrating from secure_storage_sdk ​

secure_storage_sdk is retired — new generations never offer it. If your project already generated it, that code keeps compiling and working unchanged (it's an independent copy with no live dependency back on this brick). To consolidate onto the unified SDK by hand, replace SecureStorage calls with storageProvider.forNamespace(namespace, secure: true, scope: StorageScope.user) and remove the secure_storage_sdk_api/ secure_storage_sdk_impl dependencies.

Changelog ​

Run archipelago brick info storage_sdk to see the latest published version. Enterprise customers can read the full CHANGELOG.md in the brick source repository.