# Async local storage for Angular, with validation in the read path

> The @ngx-pwa/local-storage package gives Angular apps a localStorage-shaped API backed by asynchronous indexedDB, with every operation returning an RxJS Observable and optional JSON Schema validation on reads. It suits teams that want off-main-thread storage without writing indexedDB callbacks, and it stops short of being a cache with expiration.

**cyrilletuzi/angular-async-local-storage** — Efficient client-side storage for Angular: simple API + performance + Observables + validation

- Repository: https://github.com/cyrilletuzi/angular-async-local-storage
- Stars: 677 · Forks: 67
- Language: TypeScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/cyrilletuzi-angular-async-local-storage

## The gap sits between a synchronous localStorage and a callback style indexedDB

Angular ships no client-side storage service, and almost every application ends up needing one. The two native options both disappoint in different ways. localStorage is simple, but it is synchronous, and the documentation is blunt about the consequence: if you use it too often, the app will soon begin to freeze. indexedDB is asynchronous and efficient, but it is described as a mess to work with, because it does not support Promises and you will soon be caught by callback hell. This library sits in the middle. The public API looks like the native localStorage API, the data is stored internally through the asynchronous indexedDB API, and the result is delivered through RxJS Observables. The target user is an Angular developer who wants to store user preferences, drafts or cached payloads without blocking the main thread and without writing a promise wrapper around every indexedDB call. The project is MIT licensed and the package is published on npm as @ngx-pwa/local-storage, even though the repository is named angular-async-local-storage.

## StorageMap copies the Map API, with Observables in place of return values

The service is injected like any other Angular service, and the API is modelled on the standard Map interface rather than on a custom shape:

```typescript
import { StorageMap } from '@ngx-pwa/local-storage';

@Injectable()
export class YourService {
  readonly #storageMap = inject(StorageMap);
}
```

The surface is small and easy to hold in your head. Writes are set, delete and clear, and each of them returns an Observable of undefined. Reads are get, and get with a schema. The advanced part adds watch, which emits when the stored value changes, plus size, has and keys. Since every call returns an Observable rather than a value, a write is not complete until you subscribe, and a read is not a promise of future updates. That last point is the one that surprises people, and the documentation flags it explicitly. The service is also the only object you need in application code, which is why the library can be described as Angular-like without pulling in a storage abstraction of its own.

## A get call emits once, so watching is a separate operation

The most important semantic in the library is that get returns a single value. The documentation puts it in an important callout: you will only get one value, the Observable is there for asynchrony but is not meant to emit again when the stored data is changed. If you need to follow a value, the watching guide covers the alternative, which is the watch method. The distinction exists because the underlying data lives in indexedDB, which is a separate store from your application state, and the library does not assume that every consumer wants to be woken on every write. A missing item is not an error either. Calling get for a key that was never written succeeds and emits undefined, so a component can distinguish empty from broken by checking the value rather than by catching. The observable also autocompletes, the same way the Angular HttpClient service does, so you do not have to unsubscribe. You do have to subscribe, though, and the documentation is explicit that a call without a subscription does nothing at all.

## Schema validation happens inside the read, not in your service

Because the data lives on the client, the documentation insists on one rule: always check the data, as it could have been forged. The recommended way to do that is a JSON Schema, and the schema is passed to get, which then types the emitted value:

```typescript
this.#storageMap.get('test', { type: 'string' }).subscribe({
  next: (user) => { /* Called if data is valid or `undefined` */ },
  error: (error) => { /* Called if data is invalid */ },
});
```

Validation errors travel on the error channel, which is why the subscribe call takes both a next and an error handler. A schema is also accepted by watch, so a live value keeps the same checking. The project lists typebox as the alternative to hand-written JSON Schema. This is the feature that makes the library more than a storage wrapper, because it moves the trust boundary check to the exact line where the data enters your application. The full validation guide is referenced for the range of scenarios it covers. For reads without a schema, the error channel can still be handled in the usual RxJS way, and the errors guide lists what can go wrong.

## Serialization rules decide what you are allowed to store

The write path accepts any value, but the documentation sets expectations for what goes in. Storing null or undefined makes no sense and can cause issues in some browsers, so such an item is removed instead. Beyond that you should stick to serializable JSON data: primitive types, arrays and literal objects. Date, Map, Set, Blob and other special structures can cause issues in some scenarios, and the serialization guide goes into the details. That constraint is the library's own documented rule, and it is a reasonable one, since JSON-shaped data is what most settings and drafts consist of. Storage here is persistent, the same as native localStorage and indexedDB. The expiration guide is pointed at from that section, and the documentation pushes back on a common misconception: an application that wants temporary storage like sessionStorage usually does not need it.

## Installation targets Angular 20 and above, and majors arrive often

Installing the library is a single schematic invocation, and the documentation notes the Angular floor explicitly:

```bash
# For Angular LTS (Angular >= 20):
ng add @ngx-pwa/local-storage
```

If ng add does not work, a manual installation guide is provided in the repository. The documentation also notes that Angular versions at or below 19 are officially outdated, which is the same reason the schematic is scoped to Angular 20 and above. Upgrades are handled through migration guides, and the release history shows why that matters: v22.0.1 came out on 2026-10-19, v22.0.0 on 2026-06-04 and v21.0.0 on 2025-11-20. The repository is not archived and the last push was on 2026-10-19, so the package is being worked on. The library code lives under lib/, with Angular schematics for ng add and for migrations, and the package.json shows the toolchain in use, including Angular 22 packages, RxJS 7.8, TypeScript and Playwright for the unit and end to end suites. There is a separate end to end configuration for interoperability, which is a good sign for a library that stores data another library may also read.

## When native localStorage is still the right answer

It is fair to ask why not just use localStorage. The trade-off is written into the documentation: localStorage is simple to use but synchronous, and heavy use will freeze the app, whereas indexedDB is asynchronous and efficient but painful to call. The library is the answer when you expect a meaningful amount of data, or when a synchronous write on a hot path is a real concern, or when you want the schema check on read. The cost is the Observable contract, the subscription requirement and the serialisation limits listed earlier. For a handful of small preference flags, the native API is fewer moving parts. The documentation describes the library as working around some browser issues and being tested through GitHub Actions, and the repository holds testing-apps/ directories alongside the Playwright configurations, which is the shape you would expect of a package whose correctness depends on browser storage behaviour rather than on pure logic.

## Conclusion

Adopt @ngx-pwa/local-storage when an Angular app needs more than a few kilobytes of client-side state and you want typed reads with schema validation, since the read path is where the library earns its keep. Do not adopt it for temporary per-tab state, because the project states plainly that it is persistent storage and calls the sessionStorage expectation a misconception. Before upgrading, read MIGRATION.md, since the package publishes major versions frequently, moving from v21.0.0 in November 2025 to v22.0.0 in June 2026 and v22.0.1 on 2026-10-19.

## FAQ

### How do I install @ngx-pwa/local-storage in an Angular app?

Run ng add @ngx-pwa/local-storage, which the documentation scopes to Angular LTS versions, that is Angular 20 and above. If the schematic does not work, a manual installation guide is provided in the repository.

### Does the get Observable emit again when the stored value changes?

No. The documentation states you will only get one value, the Observable exists for asynchrony and is not meant to emit again when the stored data changes. Use the watch method if you need to follow a value.

### What happens when I read a key that was never written?

It is not an error. The call succeeds and emits undefined, so a missing item is distinguished from a failure by checking the value. The error channel is used when validation fails.

### Can I store a Date or a Map in the storage?

The documentation advises sticking to serializable JSON data, meaning primitive types, arrays and literal objects, and warns that Date, Map, Set, Blob and other special structures can cause issues. Storing null or undefined removes the item instead.

### Does the library support temporary session-style storage?

No. The library is about persistent storage, the same as native localStorage and indexedDB, and the expiration guide is linked from that section. The documentation calls the desire for sessionStorage-style temporary storage a common misconception.

## Sources

- [cyrilletuzi/angular-async-local-storage on GitHub](https://github.com/cyrilletuzi/angular-async-local-storage)
- [Issues](https://github.com/cyrilletuzi/angular-async-local-storage/issues)
- [License: MIT](https://github.com/cyrilletuzi/angular-async-local-storage/blob/main/LICENSE)
- [README](https://github.com/cyrilletuzi/angular-async-local-storage/blob/main/README.md)
- [Releases](https://github.com/cyrilletuzi/angular-async-local-storage/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/cyrilletuzi-angular-async-local-storage
