# Building a Secure, Production-Ready IndexedDB Key–Value Store in Angular (With AES Encryption)

Modern enterprise Angular applications increasingly rely on **offline-first capabilities**, **client-side caching**, and **secure local persistence**. While browsers provide powerful primitives like IndexedDB and the Web Crypto API, they are often underutilized—or implemented inconsistently.

In this post, we’ll design and implement a **production-ready, generic IndexedDB key–value service for Angular**, featuring:

* Strong typing with generics
    
* AES-GCM encryption at rest
    
* Environment-based security toggles
    
* Clean Angular dependency-injection design
    
* Zero third-party storage or crypto libraries
    

This pattern is suitable for **enterprise-scale Angular applications** that require secure, reusable, and maintainable client storage.

---

## Why Not Just Use LocalStorage?

Before diving in, it’s worth clarifying **why IndexedDB is the correct choice** here:

| Feature | LocalStorage | IndexedDB |
| --- | --- | --- |
| Storage size | ~5MB | Hundreds of MBs |
| Async | ❌ No | ✅ Yes |
| Structured data | ❌ No | ✅ Yes |
| Transactions | ❌ No | ✅ Yes |
| Encryption-ready | ⚠️ Limited | ✅ Yes |
| Offline-first | ❌ Weak | ✅ Strong |

For anything beyond trivial flags or preferences, **IndexedDB is the correct foundation**.

---

## Architecture Overview

### Design Goals

This implementation focuses on:

* **Generic key–value storage** (no schema lock-in)
    
* **Encrypted data at rest**
    
* **Strong typing with TypeScript generics**
    
* **Single injectable service**
    
* **Production-only encryption**
    
* **No runtime overhead in development**
    

---

## Technology Stack

* **IndexedDB** (native browser API)
    
* **Web Crypto API** (`AES-GCM`, `PBKDF2`)
    
* **Angular Dependency Injection**
    
* **Environment-based configuration**
    

No Dexie.js. No crypto libraries. No abstractions you can’t audit.

---

## 1\. Environment Configuration

Encryption must be **explicit and environment-driven**. Development should be fast and debuggable; production should be secure.

### `environment.ts`

```ts
export const environment = {
  production: false,
  indexedDbEncryptionKey: 'DEV_SECRET_KEY'
};
```

### [`environment.prod`](http://environment.prod)`.ts`

```ts
export const environment = {
  production: true,
  indexedDbEncryptionKey: 'PROD_SECRET_KEY_CHANGE_THIS'
};
```

> ⚠️ **Important**: In real production systems, encryption keys should be injected via CI/CD secrets or runtime config—not hardcoded.

---

## 2\. Crypto Utility (AES-GCM with Web Crypto API)

We use **AES-GCM**, which provides:

* Confidentiality
    
* Integrity
    
* Authentication
    

Key derivation is handled using **PBKDF2 + SHA-256**.

### `crypto.util.ts`

```ts
export class CryptoUtil {
  private static encoder = new TextEncoder();
  private static decoder = new TextDecoder();

  static async generateKey(secret: string): Promise<CryptoKey> {
    const keyMaterial = await crypto.subtle.importKey(
      'raw',
      this.encoder.encode(secret),
      'PBKDF2',
      false,
      ['deriveKey']
    );

    return crypto.subtle.deriveKey(
      {
        name: 'PBKDF2',
        salt: this.encoder.encode('indexeddb-salt'),
        iterations: 100000,
        hash: 'SHA-256'
      },
      keyMaterial,
      { name: 'AES-GCM', length: 256 },
      false,
      ['encrypt', 'decrypt']
    );
  }

  static async encrypt(data: any, secret: string): Promise<string> {
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const key = await this.generateKey(secret);
    const encoded = this.encoder.encode(JSON.stringify(data));

    const encrypted = await crypto.subtle.encrypt(
      { name: 'AES-GCM', iv },
      key,
      encoded
    );

    return JSON.stringify({
      iv: Array.from(iv),
      data: Array.from(new Uint8Array(encrypted))
    });
  }

  static async decrypt(payload: string, secret: string): Promise<any> {
    const { iv, data } = JSON.parse(payload);
    const key = await this.generateKey(secret);

    const decrypted = await crypto.subtle.decrypt(
      { name: 'AES-GCM', iv: new Uint8Array(iv) },
      key,
      new Uint8Array(data)
    );

    return JSON.parse(this.decoder.decode(decrypted));
  }
}
```

### Why This Matters

* **AES-GCM** prevents tampering
    
* **PBKDF2** slows brute-force attacks
    
* **No external libraries** → smaller attack surface
    

---

## 3\. Generic IndexedDB Service (Angular Injectable)

This is the core of the system: a **generic, reusable IndexedDB key–value store**.

### Design Highlights

* Generic type `<T>`
    
* Single database, single object store
    
* Promise-based CRUD API
    
* Transparent encryption/decryption
    
* Centralized initialization
    

---

### `indexed-db.service.ts`

```ts
import { Injectable } from '@angular/core';
import { environment } from '../../environments/environment';
import { CryptoUtil } from './crypto.util';

@Injectable({
  providedIn: 'root'
})
export class IndexedDbService<T> {
  private readonly dbName = 'APP_DB';
  private readonly storeName = 'KEY_VALUE_STORE';
  private db!: IDBDatabase;

  constructor() {
    this.init();
  }

  private init(): void {
    const request = indexedDB.open(this.dbName, 1);

    request.onupgradeneeded = (event: any) => {
      const db = event.target.result;
      if (!db.objectStoreNames.contains(this.storeName)) {
        db.createObjectStore(this.storeName);
      }
    };

    request.onsuccess = () => {
      this.db = request.result;
    };

    request.onerror = () => {
      console.error('IndexedDB initialization failed');
    };
  }

  private getStore(mode: IDBTransactionMode): IDBObjectStore {
    const tx = this.db.transaction(this.storeName, mode);
    return tx.objectStore(this.storeName);
  }

  /* ---------------- CRUD OPERATIONS ---------------- */

  async set(key: string, value: T): Promise<void> {
    const store = this.getStore('readwrite');
    const data = environment.production
      ? await CryptoUtil.encrypt(value, environment.indexedDbEncryptionKey)
      : value;

    return new Promise((resolve, reject) => {
      const req = store.put(data as any, key);
      req.onsuccess = () => resolve();
      req.onerror = () => reject(req.error);
    });
  }

  async get(key: string): Promise<T | null> {
    const store = this.getStore('readonly');

    return new Promise((resolve, reject) => {
      const req = store.get(key);

      req.onsuccess = async () => {
        if (!req.result) {
          resolve(null);
          return;
        }

        if (environment.production) {
          const decrypted = await CryptoUtil.decrypt(
            req.result,
            environment.indexedDbEncryptionKey
          );
          resolve(decrypted as T);
        } else {
          resolve(req.result as T);
        }
      };

      req.onerror = () => reject(req.error);
    });
  }

  async delete(key: string): Promise<void> {
    const store = this.getStore('readwrite');

    return new Promise((resolve, reject) => {
      const req = store.delete(key);
      req.onsuccess = () => resolve();
      req.onerror = () => reject(req.error);
    });
  }

  async clear(): Promise<void> {
    const store = this.getStore('readwrite');

    return new Promise((resolve, reject) => {
      const req = store.clear();
      req.onsuccess = () => resolve();
      req.onerror = () => reject(req.error);
    });
  }
}
```

---

## 4\. Usage Example (Feature-Level Abstraction)

You should **never inject IndexedDB directly into components**. Instead, create **domain-specific cache services**.

### `user-cache.service.ts`

```ts
@Injectable({ providedIn: 'root' })
export class UserCacheService {
  constructor(private db: IndexedDbService<any>) {}

  saveUser(user: any) {
    return this.db.set('USER_PROFILE', user);
  }

  getUser() {
    return this.db.get('USER_PROFILE');
  }

  clearUser() {
    return this.db.delete('USER_PROFILE');
  }
}
```

This keeps:

* Storage logic centralized
    
* Domain logic explicit
    
* Refactoring painless
    

---

## 5\. Security Considerations (Read This Carefully)

### What This Is Good For

| Use Case | Recommended |
| --- | --- |
| Offline-first apps | ✅ Yes |
| API response caching | ✅ Yes |
| Large structured data | ✅ Yes |
| UI state persistence | ✅ Yes |

### What This Is **Not** For

| Data Type | Recommendation |
| --- | --- |
| JWT tokens | ❌ No |
| OAuth tokens | ❌ No |
| Refresh tokens | ❌ No |
| Long-term secrets | ❌ No |

> 🔐 **Auth data should use HttpOnly cookies or secure server-side sessions.**

---

## Optional Enhancements

If you’re taking this to the next level, consider:

* 🔄 Encryption key rotation strategy
    
* ⏳ TTL / auto-expiry support
    
* 🧬 Versioned object stores
    
* 📡 RxJS or Signals wrapper
    
* 📦 Dexie.js adapter (if allowed)
    
* 🏢 Multi-tenant / RBAC-aware key namespaces
    

---

## Final Thoughts

This pattern strikes a **pragmatic balance** between:

* Security
    
* Performance
    
* Maintainability
    
* Angular best practices
    

It avoids unnecessary dependencies, aligns with modern browser standards, and scales cleanly in enterprise environments.

---
