-
Notifications
You must be signed in to change notification settings - Fork 1
Storage
One signature over each platform's own native store. An app writes the same C# everywhere, and the value lands where that platform already keeps this kind of thing — so its backup, its migration and its uninstall all behave the way a user expects, without the framework inventing or bundling anything.
There are two interfaces, and which one you take is a decision, not a parameter.
IAppStorage |
ISecretStore |
|
|---|---|---|
| Web | localStorage |
none — resolves null |
| macOS / iOS | NSUserDefaults |
Keychain |
| Android | SharedPreferences |
KeyStore-backed AES/GCM |
Both are taken through a constructor, like every other capability:
public sealed class SettingsShell(IAppStorage storage) : StatefulComponent
{
private void Remember(string language) => storage.Set("language", language);
}Native today, web pending. Constructor injection is resolved by the .NET container, which is what constructs a component on Photon. The browser's mount does not yet do the same — it constructs a page with no arguments — so a write-once page on the web receives null for a capability it asks for this way. The web realizations exist and are registered; what is missing is the mount resolving them. Until it lands, reach storage from native code, or from web code through your own composition rather than a page constructor.
Putting a token in localStorage is the most-made security mistake on the web, and an API where
safety is a secure: true argument is an API where safety is one forgotten argument away. Here the
choice cannot be made by omission: a secret needs a different service, and asking for it is
something somebody did on purpose.
The browser has no vault, so on the web ISecretStore resolves to null — the same way the
framework reports every capability a host does not have, rather than pretending with a store any
script on the origin can read. An app that reaches for it on the web has to answer the question it
was avoiding: keep the secret on the server and hand the browser a session it can only spend, or
accept that what it was storing was never really a secret.
On native it is the platform's real vault. Apple: one kSecClassGenericPassword item per key under
the app's bundle identifier, AfterFirstUnlockThisDeviceOnly so a background refresh can read it
while a device not unlocked since boot cannot, and never synced to iCloud. Android: a
hardware-backed AES key in AndroidKeyStore — key material unreadable even by this process — with
AES/GCM ciphertext in a preferences file. That is what AndroidX's EncryptedSharedPreferences
does, written directly because the platform ships every piece and the framework does not take a
dependency for something the OS already provides.
Every store here is a string map underneath. A typed surface would be three serializers to keep identical, and their first disagreement is a value that writes on one platform and cannot be read on another. Serialize in your app, where the format is yours.
There is no server realization. During SSR the service resolves to null and reads answer
nothing.
This matters more than it sounds. A page that branches its tree on stored state renders one thing on the server and another in the browser, and the reconciler will faithfully reconcile the difference in front of the reader. Read storage in an event handler or after mount, never while building.
It is also why storage alone cannot remember a theme without a flash: the server paints first and
localStorage is not something it can see. That specific problem wants a cookie, because the
requirement is not "remember" but "tell the server".
-
localStorageis the DOM API most likely to throw rather than fail quietly — Safari's private mode has thrown on write, a browser at quota throws, and reading it at all throws when the origin has storage blocked. A preference failing to save must not take a page down, so a write that cannot happen simply does not, and a read that cannot happen answersnull. - A Keychain item does not survive every restore, and a KeyStore key is invalidated by a new lock screen. Both make every value under them permanently unreadable.
So treat a null read as "ask again" — sign in again, fall back to the default — never as an
error to report. It is the same answer a key that was never written gives, and the caller already
handles that one.
Every capability is registered with TryAdd, so registering your own wins:
builder.Services.AddSingleton<IAppStorage, MyOwnStorage>();That is the escape hatch for a store with different durability, an encrypted file, or a test double.