Web IndexedDB With Dexie

This shape gives a PWA a versioned local database, an offline mutation queue, and export/import hooks.

Database

import Dexie, { type Table } from "dexie";

export type Todo = {
  id: string;
  title: string;
  completed: boolean;
  updatedAt: string;
};

export type PendingMutation = {
  id: string;
  operation: "create" | "update" | "delete";
  entityId: string;
  payload: unknown;
  idempotencyKey: string;
  attemptCount: number;
  createdAt: string;
};

class AppDb extends Dexie {
  todos!: Table<Todo, string>;
  pendingMutations!: Table<PendingMutation, string>;

  constructor() {
    super("offline-demo");
    this.version(1).stores({
      todos: "id, updatedAt, completed",
      pendingMutations: "id, entityId, idempotencyKey, createdAt",
    });
  }
}

export const db = new AppDb();

Local Write

export async function createTodo(title: string) {
  const id = crypto.randomUUID();
  const now = new Date().toISOString();

  await db.transaction("rw", db.todos, db.pendingMutations, async () => {
    await db.todos.add({ id, title, completed: false, updatedAt: now });
    await db.pendingMutations.add({
      id: crypto.randomUUID(),
      operation: "create",
      entityId: id,
      payload: { id, title },
      idempotencyKey: `todo:create:${id}`,
      attemptCount: 0,
      createdAt: now,
    });
  });
}

Export

Use a tested export helper for real apps. For small demos, export JSON from tables:

export async function exportJson() {
  return {
    formatVersion: 1,
    exportedAt: new Date().toISOString(),
    todos: await db.todos.toArray(),
  };
}

Browser Gate

  • test quota exceeded handling
  • test blocked version upgrades with two tabs open
  • request persistent storage for critical data
  • run large exports in a worker
  • verify import on a fresh browser profile