# WrStorage

> Reactive key/value storage on top of any `Storage`-compatible engine. Swap the engine through DI (defaults to `localStorage` with an in-memory SSR / private-mode fallback). Built-in prefixing, TTL, and signal-based `watch()` for local + cross-tab updates.

Source: https://ngwr.dev/reference/services/storage  
Kind: Service

## Install

```angular-ts
import { provideWrStorage, WrStorage } from 'ngwr/storage';

bootstrapApplication(AppComponent, {
  providers: [
    // Optional — defaults to localStorage with an in-memory SSR fallback.
    provideWrStorage({ prefix: 'myapp:', ttl: 24 * 60 * 60 * 1000 }),
  ],
});
```

## Swap the engine

```angular-ts
// Swap the engine globally (e.g. sessionStorage for a tab-only app):
provideWrStorage({ engine: sessionStorage })

// Or lazily — useful when wrapping with encryption / IndexedDB / a worker bridge:
provideWrStorage({ engine: () => new EncryptedStorage(localStorage, key) })

// Or directly via the token (per-feature overrides through nested injectors):
providers: [{ provide: WR_STORAGE_ENGINE, useValue: sessionStorage }]
```

## Usage

```angular-ts
private readonly store = inject(WrStorage);

this.store.set('user', { name: 'Ada' });
this.store.get<{ name: string }>('user');         // → { name: 'Ada' }

this.store.set('cart', items, { ttl: 60_000 });   // expires in 60s
this.store.has('cart');                            // true → false after 60s

const theme = this.store.watch<'light' | 'dark'>('theme', 'light');
effect(() => console.log('theme is', theme()));
```

## What lands in storage

`set()` writes an **envelope**, not the bare value: a JSON object with the value under `v`, plus an `e` expiry in epoch milliseconds when a TTL applies. That is a wire format rather than an implementation detail, because anything reading the same key from outside Angular has to unwrap it — a pre-paint theme script in `index.html`, a service worker, a native shell, a second framework on the same origin.

```typescript
import type { WrStorageEnvelope } from 'ngwr/storage';

// provideWrStorage({ prefix: 'myapp:' })
store.set('theme', 'dark');
// localStorage['myapp:theme'] === '{"v":"dark"}'

store.set('cart', [{ sku: 'a1' }], { ttl: 60_000 });
// localStorage['myapp:cart'] === '{"v":[{"sku":"a1"}],"e":1767225600000}'

// Reading the same key from outside Angular — a pre-paint script, a worker:
const raw = localStorage.getItem('myapp:theme');
const env = raw === null ? null : (JSON.parse(raw) as WrStorageEnvelope<string>);
const value = env && (env.e === undefined || env.e >= Date.now()) ? env.v : null;
```

Three rules follow from it. A key holding anything else — a bare JSON value, or a string that is not JSON at all — is returned as it stands, so keys written by non-ngwr code keep working. A value past its `e` reads as absent and is removed on the next `get()`. And `prefix` is applied to the key on the way in, so the raw key is `prefix + key` — an outside reader has to prepend it too.

Turning the envelope off with `json: false` stores `String(value)` verbatim, which is the right choice when a key is shared with code you do not control — and gives up TTL and non-string values with it.

## Live demo

## Why ngwr provides this

Direct `localStorage` access throws in SSR and private-mode Safari, and stringly-typed keys drift. This wraps web storage with JSON (de)serialization, SSR no-ops, and typed reads.

## API

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `get(key, fallback?)` | Read a value. Returns `fallback` (default `null`) when absent or expired. | `<T>(k: string, f?: T) => T \| null` | `—` |
| `set(key, value, opts?)` | Write a value. Per-call `ttl` (ms) overrides config default. | `<T>(k: string, v: T, opts?: { ttl?: number }) => void` | `—` |
| `remove(key)` | Remove the value at `key`. | `(k: string) => void` | `—` |
| `clear()` | Clear all keys under our prefix (or everything when prefix is empty). | `() => void` | `—` |
| `has(key)` | Whether the key is present (ignoring expiry). | `(k: string) => boolean` | `—` |
| `keys()` | All known keys with the prefix stripped. | `() => readonly string[]` | `—` |
| `watch(key, fallback?)` | Reactive read. Updates on local writes and cross-tab `storage` events. | `<T>(k: string, f?: T) => Signal<T \| null>` | `—` |
| `provideWrStorage(opts?)` | Configure prefix, json, ttl, and swap the engine. | `(opts?) => EnvironmentProviders` | `—` |
| `WR_STORAGE_ENGINE` | InjectionToken for the active `Storage` engine — override at any level to swap. | `InjectionToken<Storage>` | `localStorage / memory fallback` |
| `createMemoryStorage()` | Map-backed `Storage` shim. Useful for tests. | `() => Storage` | `—` |
| `WrStorageEnvelope` | The on-disk format written while `json` is on — the value under `v`, plus an epoch-ms expiry `e` when a TTL applies. | `{ readonly v: T; readonly e?: number }` | `—` |

## See also

- [WrCookie](https://ngwr.dev/reference/services/cookie) — Same shape for `document.cookie`. Reach for cookies when you need expiry attributes / cross-subdomain reads.
- [WrTheme](https://ngwr.dev/reference/services/theme) — Built on top of WrStorage — your theme mode persists through whatever engine you swap in.
