π A modern, reactive Angular storage with AES-GCM encryption, TTL support, change notifications, and signal-based reactivity.
- π Reactive State Management - Built with Angular signals and RxJS
- π AES-GCM Encryption - Encryption with Web Crypto API
- β° TTL Support - Automatic data expiration
- π‘ Change Notifications - Real-time storage change watching
- πͺ Multi-Storage Support - localStorage and sessionStorage
- π― Multiple Instances - Named storage configurations
- π Storage Analytics - Usage statistics and monitoring
- π‘οΈ Type Safe - Full TypeScript support with generics
- π Cross-Browser - Graceful fallbacks for older browsers
npm install ngx-browser-storage
This library follows Angular's versioning for clear compatibility:
Angular Version | ngx-browser-storage Version | Status |
---|---|---|
20.0.x | 20.0.x | β Current |
20.1.x | 20.1.x | π Planned |
// app.config.ts
import { ApplicationConfig } from "@angular/core";
import { provideNgxBrowserStorage } from "ngx-browser-storage";
export const appConfig: ApplicationConfig = {
providers: [
provideNgxBrowserStorage({
prefix: "myapp",
storageType: "localStorage",
defaultTTL: 60, // 1 hour
enableLogging: false,
}),
],
};
import { Component, inject, signal } from "@angular/core";
import { NgxBrowserStorageService } from "ngx-browser-storage";
@Component({
selector: "app-user-profile",
template: `
<div>
<input [(ngModel)]="username" placeholder="Username" />
<button (click)="saveUser()">Save</button>
<button (click)="saveUserSecure()">Save Encrypted</button>
@if (currentUser(); as user) {
<p>Welcome, {{ user.name }}!</p>
}
<p>Items in storage: {{ storage.stats().itemCount }}</p>
</div>
`,
})
export class UserProfileComponent {
private storage = inject(NgxBrowserStorageService);
username = "";
currentUser = signal<User | null>(null);
async ngOnInit() {
// Create reactive signal for user data
this.currentUser = await this.storage.createSignal<User>("currentUser");
}
async saveUser() {
const user = { name: this.username, id: Date.now() };
await this.storage.setData("currentUser", user);
}
async saveUserSecure() {
const user = { name: this.username, id: Date.now() };
await this.storage.setData("currentUser", user, {
encrypt: true,
ttlMinutes: 60,
});
}
}
NgxBrowserStorage provides encryption using the Web Crypto API:
// Store sensitive data with encryption
await storage.setData("apiToken", "secret-key-123", {
encrypt: true,
ttlMinutes: 120, // Expires in 2 hours
});
// Retrieve encrypted data
const token = await storage.getData("apiToken", {
decrypt: true,
defaultValue: null,
});
// Check encryption support
if (storage.isEncryptionSupported()) {
console.log("Using AES-GCM encryption");
} else {
console.log("Falling back to Base64 encoding");
}
Feature | Base64 Fallback | AES-GCM Encryption |
---|---|---|
Key Length | None | 256-bit |
Data Integrity | β | β Authenticated |
Tamper Protection | β | β Auth Tags |
Browser Support | Universal | Modern + Fallback |
import { provideNgxBrowserStorage } from "ngx-browser-storage";
export const appConfig: ApplicationConfig = {
providers: [
provideNgxBrowserStorage({
prefix: "myapp",
storageType: "localStorage",
defaultTTL: 0, // No expiration
enableLogging: false,
caseSensitive: false,
}),
],
};
import { provideNgxBrowserStorage } from "ngx-browser-storage";
import { environment } from "./environments/environment";
export const appConfig: ApplicationConfig = {
providers: [
provideNgxBrowserStorage(
() => ({
prefix: environment.production ? "prod-app" : "dev-app",
storageType: environment.production ? "localStorage" : "sessionStorage",
defaultTTL: environment.production ? 60 : 30,
enableLogging: !environment.production,
caseSensitive: false,
}),
{
autoCleanup: true,
strictMode: environment.production,
enableMetrics: !environment.production,
}
),
],
};
import { provideNamedNgxBrowserStorage, NgxBrowserStorageManager } from "ngx-browser-storage";
export const appConfig: ApplicationConfig = {
providers: [
provideNamedNgxBrowserStorage(() => ({
user: {
prefix: "user-data",
storageType: "localStorage",
defaultTTL: 0, // Persistent
enableLogging: false,
},
cache: {
prefix: "app-cache",
storageType: "localStorage",
defaultTTL: 60, // 1 hour
enableLogging: false,
},
session: {
prefix: "session-data",
storageType: "sessionStorage",
defaultTTL: 30, // 30 minutes
enableLogging: true,
},
})),
NgxBrowserStorageManager,
],
};
Store data with optional encryption and TTL.
// Basic storage
await storage.setData("user", { name: "John", age: 30 });
// With encryption and TTL
await storage.setData("session", userData, {
encrypt: true,
ttlMinutes: 60,
});
Retrieve data with optional decryption.
// Basic retrieval
const user = await storage.getData<User>("user");
// With decryption and default value
const theme = await storage.getData("theme", {
decrypt: true,
defaultValue: "light",
});
Check if a key exists in storage.
const userExists = await storage.hasKey("currentUser");
Remove a specific key from storage.
storage.removeData("temporaryData");
Clear all storage data with the current prefix.
storage.removeAll();
Create a reactive signal that automatically updates when storage changes.
// Create reactive signals
const userSignal = await storage.createSignal<User>("currentUser");
const themeSignal = await storage.createSignal("theme", "light");
// Use in template
@Component({
template: `<p>Hello {{ userSignal()?.name }}!</p>`,
})
export class MyComponent {
userSignal = signal<User | null>(null);
async ngOnInit() {
this.userSignal = await inject(NgxBrowserStorageService).createSignal<User>("user");
}
}
Watch for changes to a specific key.
// Watch single key
storage.watch<string>("theme").subscribe((theme) => {
document.body.className = theme || "light";
});
Watch for all storage changes.
storage.watchAll().subscribe((event) => {
console.log(`${event.action} on ${event.key}:`, event.newValue);
});
Watch multiple specific keys.
storage.watchKeys(["user", "settings", "theme"]).subscribe(({ key, value }) => {
console.log(`${key} changed:`, value);
});
Watch keys matching a pattern.
// Watch all user-related keys
storage.watchPattern("user.*").subscribe(({ key, value }) => {
console.log(`User data ${key} changed:`, value);
});
Update existing data using a function.
await storage.updateData("cart", (current: CartItem[] = []) => [...current, newItem], { encrypt: true });
Set data only if key doesn't exist.
const wasSet = await storage.setIfNotExists("config", defaultConfig);
Get detailed storage statistics.
const stats = await storage.getStorageStats();
console.log(`Total: ${stats.totalItems} items, ${stats.totalSize} bytes`);
Check if AES-GCM encryption is available.
if (storage.isEncryptionSupported()) {
// Use full encryption
} else {
// Handle fallback
}
Clear cached encryption key (useful for logout).
// Clear encryption key for security
storage.clearEncryptionKey();
@Injectable({ providedIn: "root" })
export class AuthService {
private storage = inject(NgxBrowserStorageService);
// Reactive authentication state
isAuthenticated = computed(() => this.storage.stats().keys.includes("auth"));
currentUser = signal<User | null>(null);
async ngOnInit() {
this.currentUser = await this.storage.createSignal<User>("currentUser");
}
async login(credentials: LoginCredentials): Promise<void> {
const result = await this.authApi.login(credentials);
// Store encrypted auth token with 8-hour TTL
await this.storage.setData("auth", result.token, {
encrypt: true,
ttlMinutes: 8 * 60,
});
await this.storage.setData("currentUser", result.user);
}
async logout(): Promise<void> {
this.storage.removeMultiple(["auth", "currentUser"]);
this.storage.clearEncryptionKey();
}
}
@Injectable({ providedIn: "root" })
export class CartService {
private storage = inject(NgxBrowserStorageService);
// Reactive cart state
items = signal<CartItem[]>([]);
itemCount = computed(() => this.items().reduce((sum, item) => sum + item.quantity, 0));
total = computed(() => this.items().reduce((sum, item) => sum + item.price * item.quantity, 0));
async ngOnInit() {
this.items = await this.storage.createSignal<CartItem[]>("cart", []);
}
async addItem(product: Product, quantity = 1): Promise<void> {
await this.storage.updateData(
"cart",
(current: CartItem[] = []) => {
const existing = current.find((item) => item.id === product.id);
if (existing) {
existing.quantity += quantity;
return [...current];
}
return [...current, { ...product, quantity }];
},
{ encrypt: true, ttlMinutes: 60 }
);
}
async removeItem(productId: string): Promise<void> {
await this.storage.updateData("cart", (current: CartItem[] = []) => current.filter((item) => item.id !== productId), { encrypt: true });
}
clearCart(): void {
this.storage.removeData("cart");
}
}
@Injectable({ providedIn: "root" })
export class PreferencesService {
private storage = inject(NgxBrowserStorageService);
// Reactive preferences
theme = signal<"light" | "dark">("light");
language = signal<string>("en");
notifications = signal<boolean>(true);
async ngOnInit() {
// Initialize reactive preferences
this.theme = await this.storage.createSignal("theme", "light");
this.language = await this.storage.createSignal("language", "en");
this.notifications = await this.storage.createSignal("notifications", true);
// Auto-apply theme changes
effect(() => {
document.body.setAttribute("data-theme", this.theme());
});
}
async updatePreference<T>(key: string, value: T): Promise<void> {
await this.storage.setData(key, value, { encrypt: true });
}
async resetToDefaults(): Promise<void> {
await this.updatePreference("theme", "light");
await this.updatePreference("language", "en");
await this.updatePreference("notifications", true);
}
}
@Injectable({ providedIn: "root" })
export class FormAutoSaveService {
private storage = inject(NgxBrowserStorageService);
private saveTimeouts = new Map<string, number>();
async autoSave<T>(formId: string, data: T, delayMs = 1000): Promise<void> {
// Debounce saves
const existingTimeout = this.saveTimeouts.get(formId);
if (existingTimeout) {
clearTimeout(existingTimeout);
}
const timeoutId = setTimeout(async () => {
await this.storage.setData(
`form_${formId}`,
{
data,
savedAt: Date.now(),
},
{
encrypt: true,
ttlMinutes: 60,
}
);
this.saveTimeouts.delete(formId);
}, delayMs);
this.saveTimeouts.set(formId, timeoutId);
}
async getSavedData<T>(formId: string): Promise<{ data: T; savedAt: number } | null> {
return await this.storage.getData(`form_${formId}`, { decrypt: true });
}
clearSavedData(formId: string): void {
const timeoutId = this.saveTimeouts.get(formId);
if (timeoutId) {
clearTimeout(timeoutId);
this.saveTimeouts.delete(formId);
}
this.storage.removeData(`form_${formId}`);
}
}
@Component({
selector: "app-dashboard",
})
export class DashboardComponent {
private storageManager = inject(NgxBrowserStorageManager);
// Different storage instances for different purposes
private userStorage = this.storageManager.getStorage("user");
private cacheStorage = this.storageManager.getStorage("cache");
private sessionStorage = this.storageManager.getStorage("session");
async ngOnInit() {
// Load persistent user data
const userProfile = await this.userStorage.getData("profile");
// Load cached application data
const cachedData = await this.cacheStorage.getData("dashboard-metrics");
// Save current session state
await this.sessionStorage.setData("current-view", "dashboard", {
encrypt: true,
ttlMinutes: 30,
});
}
}
import { provideNgxBrowserStorage } from "ngx-browser-storage";
// test-setup.ts
export function provideStorageForTesting() {
return provideNgxBrowserStorage(
{
prefix: "test",
storageType: "sessionStorage",
defaultTTL: 0,
enableLogging: true,
caseSensitive: false,
},
{
autoCleanup: false,
strictMode: true,
enableMetrics: false,
}
);
}
// In test files
describe("NgxBrowserStorageService", () => {
let service: NgxBrowserStorageService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [...provideStorageForTesting()],
});
service = TestBed.inject(NgxBrowserStorageService);
});
it("should store and retrieve data", async () => {
await service.setData("test", "value");
const result = await service.getData("test");
expect(result).toBe("value");
});
it("should handle encryption", async () => {
await service.setData("secret", "encrypted-data", { encrypt: true });
const result = await service.getData("secret", { decrypt: true });
expect(result).toBe("encrypted-data");
});
it("should create reactive signals", async () => {
const signal = await service.createSignal<string>("reactive-test", "default");
expect(signal()).toBe("default");
await service.setData("reactive-test", "updated");
expect(signal()).toBe("updated");
});
});
This is a breaking change that requires updates:
# Remove old package
npm uninstall ng7-storage
# Install new package
npm install ngx-browser-storage
// Before
import { provideNgStorageConfig } from "ng7-storage";
// After
import { provideNgxBrowserStorage } from "ngx-browser-storage";
// Before (synchronous)
const user = storage.getData("user");
storage.setData("user", newUser);
// After (asynchronous)
const user = await storage.getData("user");
await storage.setData("user", newUser);
// Before
const signal = storage.createSignal("user");
// After
const signal = await storage.createSignal("user");
The default prefix changed from ng-storage
to ngx-browser-storage
. Existing data with the old prefix won't be automatically migrated. You can:
- Keep using a custom prefix:
prefix: 'ng-storage'
- Or migrate data manually in your application
β οΈ Package Name:ng7-storage
βngx-browser-storage
β οΈ Method Signatures: Core methods now asyncβ οΈ Signal Creation: Now asyncβ οΈ Default Prefix:ng-storage
βngx-browser-storage
- β Enhanced: Much stronger AES-GCM encryption
- β Compatible: Existing stored data still readable
Browser | Version | Storage Support | AES-GCM Encryption |
---|---|---|---|
Chrome | 37+ | β | β |
Firefox | 34+ | β | β |
Safari | 7+ | β | β |
Edge | 12+ | β | β |
IE | 11 | β | β (Base64 fallback) |
IE | 8-10 | β | β (Base64 fallback) |
We welcome contributions! Please see our Contributing Guide for details.
# Clone repository
git clone https://github.com/edisonaugusthy/ng-storage.git
# Install dependencies
npm install
# Run tests
npm test
# Build library
npm run build
# Check Angular version alignment
npm run check-angular-version
- Package renamed:
ng7-storage
βngx-browser-storage
- Methods now async:
setData()
,getData()
,hasKey()
for encryption support - Signal creation async:
createSignal()
now returnsPromise<Signal<T>>
- Default prefix changed:
ng-storage
βngx-browser-storage
- AES-GCM Encryption: Military-grade security with Web Crypto API
- PBKDF2 Key Derivation: 100,000 iterations for secure key generation
- Version Alignment: Now follows Angular version numbers (20.0.3)
- Enhanced Security: Data integrity protection with authentication tags
- Better Fallbacks: Automatic Base64 fallback for older browsers
- Improved TypeScript: Better type safety and generics
- Unique Encryption: Random IVs ensure unique encryption per data item
- Tamper Protection: Authentication tags prevent data modification
- Key Management: Secure key caching and rotation support
- Browser Detection: Automatic encryption capability detection
- Performance: Optimized key caching and encryption operations
- Error Handling: Better error messages and fallback strategies
- Documentation: Comprehensive guides and examples
- Testing: Enhanced test coverage and utilities
- v19.0.0: Previous version with basic Base64 encoding
- v1.0.0: Initial release
This project is licensed under the MIT License - see the LICENSE file for details.
- Angular team for the amazing framework and signals
- Web Crypto API for enabling secure client-side encryption
- RxJS team for reactive programming utilities
- Community contributors and users
- π Bug Reports: GitHub Issues
- π¬ Discussions: GitHub Discussions
- π Security Issues: Please report security vulnerabilities privately via email
Made with β€οΈ for the Angular community