Skip to main content

Command Palette

Search for a command to run...

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

Updated
5 min read
S

Technology Enthusiast and voracious reader with a demonstrated history of working in the computer software industry. Skilled in PHP, JavaScript, NodeJS, Angular, MySQL, MongoDB, Web3, Product Development, Project Management, and Teamwork.

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:

FeatureLocalStorageIndexedDB
Storage size~5MBHundreds 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

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

environment.prod.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

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

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

@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 CaseRecommended
Offline-first apps✅ Yes
API response caching✅ Yes
Large structured data✅ Yes
UI state persistence✅ Yes

What This Is Not For

Data TypeRecommendation
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.