diff --git a/frontend/libs/components/video-editor/src/lib/adapters/default/indexeddb-project-storage.ts b/frontend/libs/components/video-editor/src/lib/adapters/default/indexeddb-project-storage.ts
--- a/frontend/libs/components/video-editor/src/lib/adapters/default/indexeddb-project-storage.ts
+++ b/frontend/libs/components/video-editor/src/lib/adapters/default/indexeddb-project-storage.ts
@@ -43,7 +43,7 @@ function tx<T>(
         t.oncomplete = () => resolve(result as T);
         t.onerror = () => reject(t.error);
         t.onabort = () => reject(t.error);
-      }),
+      }).finally(() => db.close()),
   );
 }
 
diff --git a/frontend/libs/components/video-editor/tests/README.md b/frontend/libs/components/video-editor/tests/README.md
new file mode 100644
--- /dev/null
+++ b/frontend/libs/components/video-editor/tests/README.md
@@ -0,0 +1,29 @@
+# Native IndexedDB storage checks
+
+`indexeddb-project-storage.browser.html` exercises the actual storage adapter in a
+browser using native IndexedDB requests, transactions, structured cloning and
+version upgrades. It routes the adapter's database name to a dedicated test
+namespace and deletes that test database between cases. The application database
+is not touched.
+
+From `frontend/`, compile only the adapter and serve the harness:
+
+```sh
+idb_check_dir=$(mktemp -d /tmp/artcraft-idb-check.XXXXXX)
+node node_modules/esbuild/bin/esbuild \
+  libs/components/video-editor/src/lib/adapters/default/indexeddb-project-storage.ts \
+  --format=esm --target=es2022 --outfile="$idb_check_dir/indexeddb-storage.js"
+cp libs/components/video-editor/tests/indexeddb-project-storage.browser.html \
+  "$idb_check_dir/index.html"
+python3 -m http.server 0 --bind 127.0.0.1 --directory "$idb_check_dir"
+```
+
+Open the local address and port printed by the server. All six cases should report
+`PASS`. The final JSON is also available as `globalThis.runResult` for a browser
+runner to collect. Stop the server and remove the generated directory afterward.
+
+The two round-trip controls verify public storage behavior. The remaining checks
+verify connection cleanup after successful operations, a blocked native version
+upgrade, a synchronous `DataCloneError`, and an aborted native transaction. This
+harness uses browser APIs because the library's default jsdom test environment
+does not provide IndexedDB. It does not replace the ordinary library test suite.
diff --git a/frontend/libs/components/video-editor/tests/indexeddb-project-storage.browser.html b/frontend/libs/components/video-editor/tests/indexeddb-project-storage.browser.html
new file mode 100644
--- /dev/null
+++ b/frontend/libs/components/video-editor/tests/indexeddb-project-storage.browser.html
@@ -0,0 +1,135 @@
+<!doctype html>
+<meta charset="utf-8" />
+<title>Native IndexedDB project-storage checks</title>
+<pre id="results">Running native IndexedDB checks...</pre>
+<script type="module">
+  import { createIndexedDBProjectStorage } from "./indexeddb-storage.js";
+
+  // Route the adapter to a dedicated test database. Transactions, structured
+  // cloning, error events, aborts, and version upgrades remain native browser APIs.
+  const TEST_DB = "artcraft-project-storage-browser-check";
+  const nativeOpen = indexedDB.open.bind(indexedDB);
+  const nativeClose = IDBDatabase.prototype.close;
+  const nativePut = IDBObjectStore.prototype.put;
+  const connections = new Set();
+  const results = [];
+  const storage = createIndexedDBProjectStorage();
+  const project = {
+    id: "native-proof", name: "Native", updatedAt: 100, data: { value: 1 },
+  };
+
+  indexedDB.open = (name, ...args) => {
+    const request = nativeOpen(name === "video-editor" ? TEST_DB : name, ...args);
+    request.addEventListener("success", () => connections.add(new WeakRef(request.result)));
+    return request;
+  };
+  IDBDatabase.prototype.close = function () {
+    for (const ref of connections) {
+      if (ref.deref() === this) connections.delete(ref);
+    }
+    return nativeClose.call(this);
+  };
+
+  await check("control: save/load/list/delete round trip", async () => {
+    await storage.saveProject(project);
+    expect(await storage.loadProject(project.id), project, "saved data");
+    expect(await storage.listProjects(), [
+      { id: project.id, name: project.name, updatedAt: 100 },
+    ], "list");
+    await storage.deleteProject(project.id);
+    expect(await storage.loadProject(project.id), null, "deleted data");
+  });
+
+  await check("success: settled save/load/list release connections", async () => {
+    await storage.saveProject(project);
+    await storage.loadProject(project.id);
+    await storage.listProjects();
+    expect([...connections].filter((ref) => ref.deref()).length, 0, "open connections after settled operations");
+  });
+
+  await check("success: completed storage does not block an upgrade", async () => {
+    await storage.saveProject(project);
+    await new Promise((resolve, reject) => {
+      const request = nativeOpen(TEST_DB, 2);
+      request.onblocked = () => {
+        reject(new Error("Upgrade blocked by a completed storage operation"));
+        for (const ref of [...connections]) ref.deref()?.close();
+      };
+      request.onerror = () => reject(request.error);
+      request.onsuccess = () => {
+        request.result.close();
+        resolve();
+      };
+    });
+  });
+
+  await check("request construction failure releases its connection", async () => {
+    let error;
+    try {
+      await storage.saveProject({ ...project, data: () => {} });
+    } catch (caught) {
+      error = caught;
+    }
+    expect(error?.name, "DataCloneError", "native cloning error");
+    expect([...connections].filter((ref) => ref.deref()).length, 0, "open connections after failed request construction");
+  });
+
+  await check("native transaction abort releases connection and writes nothing", async () => {
+    IDBObjectStore.prototype.put = function (...args) {
+      const request = nativePut.apply(this, args);
+      this.transaction.abort();
+      return request;
+    };
+    let error;
+    try {
+      await storage.saveProject(project);
+    } catch (caught) {
+      error = caught;
+    }
+    if (!error) throw new Error("Aborted save must reject");
+    expect([...connections].filter((ref) => ref.deref()).length, 0, "open connections after aborted write");
+    IDBObjectStore.prototype.put = nativePut;
+    expect(await storage.loadProject(project.id), null, "aborted data");
+  });
+
+  await check("control: create and delete public methods", async () => {
+    const created = await storage.createProject("Created");
+    expect((await storage.loadProject(created.id))?.name, "Created", "created name");
+    await storage.deleteProject(created.id);
+    expect(await storage.loadProject(created.id), null, "deleted created project");
+  });
+
+  indexedDB.open = nativeOpen;
+  IDBDatabase.prototype.close = nativeClose;
+  IDBObjectStore.prototype.put = nativePut;
+  globalThis.runResult = {
+    userAgent: navigator.userAgent,
+    origin: location.origin,
+    nativeIndexedDB: typeof IDBFactory === "function",
+    results,
+  };
+  document.querySelector("#results").textContent = JSON.stringify(globalThis.runResult, null, 2);
+
+  async function check(name, run) {
+    try {
+      await run();
+      results.push({ name, status: "PASS" });
+    } catch (error) {
+      results.push({ name, status: "FAIL", error: String(error) });
+    } finally {
+      for (const ref of [...connections]) ref.deref()?.close();
+      IDBObjectStore.prototype.put = nativePut;
+      await new Promise((resolve, reject) => {
+        const request = indexedDB.deleteDatabase(TEST_DB);
+        request.onsuccess = resolve;
+        request.onerror = () => reject(request.error);
+      });
+    }
+  }
+
+  function expect(value, expected, label) {
+    if (JSON.stringify(value) !== JSON.stringify(expected)) {
+      throw new Error(`${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(value)}`);
+    }
+  }
+</script>
