Every app needs to persist something in the browser: user preferences, a sidebar toggle, a short-lived session token. localStorage and sessionStorage are the default tools for that.
But using the raw Web Storage API in Angular still feels like 2015. You end up writing the same boilerplate every time.
Most storage wrappers give you a getter and a setter and call it done. Reactivity, encryption, expiration — you're on your own for all three.
So I built ngx-local-vault: persistence, encryption, and TTL, wired into a native Angular WritableSignal.
What's Actually Wrong with Browser Storage
If you've built anything non-trivial in Angular, at least one of these has bitten you:
-
No reactivity.
localStorage.setItem()doesn't trigger change detection. You end up writing your own events, or an RxJS Subject, just so a component notices the value changed. -
Plain text. JWTs and profile data sitting in
localStoragein plain text are one DevTools tab (or one XSS script) away from being read. -
No expiry.
localStoragejust sits there forever. Want a token to die after 15 minutes? Store a timestamp yourself, check it on load, delete it yourself. -
SSR headaches. Touch
window.localStorageduring server-side rendering and you getReferenceError: window is not defined. Wrapping every access inisPlatformBrowsergets old fast.
ngx-local-vault
It's a small library — under 2KB gzipped, no dependencies beyond Angular itself — that treats browser storage as something a component can just read reactively.
Instead of separate read and write calls, there's a watchSignal() method that hands you back a real Angular Signal. Read it, .set() it, .update() it — encryption and TTL happen underneath without you thinking about them.
1. Setup
Add it to app.config.ts. Works with Angular 17 through 20.
import { ApplicationConfig } from '@angular/core';
import { provideVault } from 'ngx-local-vault';
export const appConfig: ApplicationConfig = {
providers: [
provideVault({
prefix: 'app_',
encryptionKey: 'your-secure-key',
driver: 'local'
})
]
};
2. Using it
Inject VaultService and watch a key:
import { inject, Component } from '@angular/core';
import { VaultService } from 'ngx-local-vault';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
standalone: true
})
export class AppComponent {
private vault = inject(VaultService);
theme = this.vault.watchSignal<'light' | 'dark'>('theme', 'light');
session = this.vault.watchSignal<string | null>('session-token', null, {
expiresIn: '15m'
});
toggleTheme() {
this.theme.update(current => current === 'light' ? 'dark' : 'light');
}
login(token: string) {
this.session.set(token);
}
}
What it does differently
-
Real signals.
watchSignal()returns an actualWritableSignal<T>, not some wrapper you have to learn. - Encrypted at rest. Open the Application tab in DevTools and you won't find raw JSON or a bare token sitting there.
-
TTL that actually deletes itself. Pass
expiresIn: '15m'and the entry disappears in-tab when the clock runs out — no reload needed. It takes plain strings:500ms,30s,15m,2h,1d. -
SSR-safe. Guarded by
PLATFORM_ID, so on the server it's just a no-op. No hydration mismatches. -
Actually zero dependencies. Only
@angular/coreand@angular/commonas peers. Eventslibgets stripped at build time.
Try It
- Live demo: ysndmr.github.io/ngx-local-vault
- NPM: npmjs.com/package/ngx-local-vault
- GitHub: github.com/ysndmr/ngx-local-vault
- My site: ysndmr.com
- Original post: dev.to
If you're not on Angular, there are React and Vue versions too — links are in the GitHub README.
Wrap Up
Signals changed how Angular handles state. I think storage should have followed the same shift a while ago.
How are you handling browser storage in your Angular apps right now? Open an issue or drop a star on GitHub if this is useful to you.
Top comments (0)